Android基于DataBinding+Koin实现MVVM模式页面快速开发框架

1. 前言

上一篇介绍了 ardf(android rapid development framework,Android 快速开发框架) 基于 DataBinding 对 RecyclerView 的封装实现和使用,ardf目的是封装一系列 Android 开发框架帮助开发者快速开发提高开发效率。本篇是 ardf的第二篇,将介绍基于 DataBinding + Koin 实现的 MVVM 模式页面快速开发框架的使用和详细实现。

Android基于DataBinding封装RecyclerView实现快速列表开发

DataBinding 是 Google 官方的一个数据绑定框架,借助该库,您可以声明式的将应用中的数据源绑定到布局中的界面组件上,实现通过数据驱动界面更新,从而降低布局和逻辑的耦合性,使代码逻辑更加清晰。更多关于 DataBinding 的介绍请查阅 Google 官方文档:DataBinding

Koin 是一个基于 Kotlin 的 DSL 实现的轻量级依赖注入框架,相比于 Dagger2, Koin 无反射、无代码生成且使用更简单;借助该库可轻松在基于 kotlin 的 Android 应用开发中实现依赖注入,降低代码的耦合性。更多关于 Koin 的介绍及使用请查阅官方文档:Koin

2. 使用效果

在 Android 应用中页面显示几乎是每个应用必不可少的功能,要让页面布局在手机上进行显示绝大多数情况都是使用 Activity/Fragment 来承载;而创建一个 Activity/Fragment 需要先加载布局,然后从布局中找到我们需要的 View 对象再去更新其数据或为其添加相应事件处理,那么如果将这些封装成通用的 Activity/Fragment 基类则将减少很多开发代码从而提高开发效率。

先看一下封装后的代码使用效果。

2.1 项目配置

在项目 Module 的 build.gradle 中添加依赖,如下:

dependencies {
    implementation 'com.loongwind.ardf:base:1.0.1'
}

ardf基于 DataBinding 进行封装,需要开启 DataBinding,启用方式如下:

android {
    ...
    buildFeatures {
        dataBinding true
    }
}

同时在插件中添加 kotlin-kapt的插件,如下:

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    // 添加 kotlin-kapt 插件
    id 'kotlin-kapt'
}

配置完成后,点击 Sync Now同步 build.gradle 配置生效后即可进行代码开发。

2.2 自动装载布局

通过继承 ardf提供的 BaseBindingActivity/ BaseBindingFragment可快速装载页面布局。

在 layout 里创建一个 test_page.xml的布局文件:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
  
    <data>
        <variable
            name="text"
            type="String" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="10dp">

       <TextView
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:padding="15dp"
            android:textSize="30sp"
            android:text="@{text}"/> //绑定数据

    </LinearLayout>
</layout>

然后创建一个 TestActivity:

//泛型类型是布局通过 DataBinding 自动生成的 ViewDataBinding 类型
class TestActivity : BaseBindingActivity<TestPageBinding>() {
   
    // 通过 binding 操作界面元素更新界面
    override fun initDataBinding(binding: TestPageBinding) {
        binding.text = "Hello ardf"
    }
}

代码完成了,只需继承 BaseBindingActivity泛型填写布局自动生成的 Binding 类,然后在实现的 initDataBinding方法中绑定界面数据即可。

运行效果如下:

同样 Fragment 的使用方法类似,创建一个 TestFragment :

//泛型类型是布局通过 DataBinding 自动生成的 ViewDataBinding 类型
class TestFragment : BaseBindingFragment<TestPageBinding>() {
    
    // 通过 binding 操作界面元素更新界面
    override fun initDataBinding(binding: TestPageBinding) {
        binding.text = "Hello ardf"
    }
}

运行效果跟 Activity 一样,这里就不重复贴图了。

2.3 自动注入 ViewModel

ardf除了自动装载布局以外,还支持自动注入 ViewModel 并将 ViewModel 与界面布局自动进行绑定。

首先创建一个 TestViewModel 继承自 BaseViewModel

class TestViewModel : BaseViewModel(){
    val text = "Hello ardf ViewModel"
}

修改上面的 test_page.xml 布局接收vm变量的 TestViewModel 数据:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">
  
    <data>

        <!--通过 DataBinding 接收 ViewModel 对象-->
        <variable
            name="vm"
            type="com.loongwind.ardf.demo.TestViewModel" />

    </data>
  
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        android:padding="10dp">

       <TextView
            android:id="@+id/text"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:gravity="center"
            android:textSize="30sp"
            android:text="@{vm.text}" //绑定 TestViewModel 中的 text 数据
            android:padding="15dp"/>

    </LinearLayout>
</layout>

通过 DataBinding 方式将 ViewModel 中的数据绑定到界面元素中。

然后再创建 TestActivity 继承自 BaseBindingViewModelActivity

//第一个泛型类型是布局通过 DataBinding 自动生成的 ViewDataBinding 类型
//第二个泛型就是上面创建的 ViewModel 类型
class TestActivity : BaseBindingViewModelActivity<TestPageBinding, TestViewModel>() {

}

可以发现,Activity 的代码又简介了许多。

最后一步是实现 ViewModel 的注入,ardf基于 koin实现依赖注入,需要创建 appModule 将 实现的 TestViewModel 添加到依赖中,然后在 Application 中初始化 koin,代码如下:

val appModule = module {
    // 将 ViewModel 添加到 koin 依赖
    viewModel{ TestViewModel()}
}

class App : Application() {

    override fun onCreate() {
        super.onCreate()
        // 启动 koin
        startKoin{
            androidLogger()
            androidContext(this@App)
            // 添加 appModule
            modules(appModule)
        }
    }
}

代码实现完成,运行效果如下:

跟之前的实现效果一致,同样的 Fragment 使用方法是一样的,只需继承 BaseBindingViewModelFragment即可,如下:

//第一个泛型类型是布局通过 DataBinding 自动生成的 ViewDataBinding 类型
//第二个泛型就是上面创建的 ViewModel 类型
class TestFragment : BaseBindingViewModelFragment<TestPageBinding, TestViewModel>() {

}

2.4 事件处理

前面界面加载完成了,数据也可以在 ViewModel 中进行更新,常规事件也可以在 ViewModel 中进行处理,但是跟 Context 相关的处理在 ViewModel 中是没办法进行处理的,因为 ViewModel 中没办法拿到 Context 实例,比如 toast 提示、弹框、页面跳转等,这些情况怎么处理呢?

ardf提供了事件的处理机制,可以将事件传递到 Activity / Fragment 中,然后在 Activity / Fragment 中进行涉及 Context 的处理,并且 ardf提供了两种事件的默认处理:toast(弹出 toast 提示)、back(返回上一个页面)。

2.4.1 toast 提示

BaseViewModel 的子类中调用 postHintText即可在界面上弹出对应的 toast 提示:

class TestViewModel : BaseViewModel(){
    val text = "Hello ardf ViewModel"
    
    fun showToastString(){
        //传入字符串
        postHintText("Hello ardf toast")
    }
    
    fun showToastStringRes(){
        //传入字符串资源
        postHintText(R.string.hello)
    }
}

在布局里添加两个按钮,事件绑定对应的 showToast 方法,运行效果:

2.4.2 back 返回

BaseViewModel 的子类中调用 back()方法即可:

class TestViewModel : BaseViewModel(){
    val text = "Hello ardf ViewModel"
    
    fun goBack(){
        //调用父类提供的 back 方法
        back()
    }
}

同样在布局里添加按钮事件触发 goBack 方法,运行效果如下:

目前 back 方法只在 BaseBindingViewModelActivity 宿主的 BaseViewModel 子类中使用下有效

2.4.3 自定义事件

自定义事件可通过调用 postEvent方法将事件传递到 Activity / Fragment 中,代码如下:

class TestViewModel : BaseViewModel(){
    val text = "Hello ardf ViewModel"
    companion object {
        // 定义跳转到详情页的事件 id
        const val EVENT_TO_DETAILS = 0x00
        // 定义弹出 Dialog 的事件 id
        const val EVENT_SHOW_DIALOG = 0x01
    }
    
    fun toDetailsPage(){
        // 发送跳转详情页事件
        postEvent(EVENT_TO_DETAILS)
    }
    
    fun showDialog(){
        // 发送弹出 Dialog 事件
        postEvent(EVENT_SHOW_DIALOG)
    }
}

然后在 Activity / Fragment 中重写 onEvent方法接收事件进行相应处理:

class TestActivity : BaseBindingViewModelActivity<TestPageBinding, TestViewModel>() {
    
    // 接收事件
    override fun onEvent(eventId: Int) {
        super.onEvent(eventId)
        // 判断事件 id 并进行对应处理
        when(eventId){
            TestViewModel.EVENT_TO_DETAILS -> startActivity(Intent(this, DetailsActivity::class.java))
            TestViewModel.EVENT_SHOW_DIALOG -> showXxxDialog()
        }

    }
}

运行效果如下:

3. 源码解析

前面介绍了 ardf实现自动装载布局、自动注入 ViewModel 和事件的处理的使用,那么 ardf是如何实现这些功能的呢?

首先来看一下 ardf关于页面封装的整体结构,如下:

主要分为四层:依赖库、基础支撑、布局自动绑定、ViewModel 自定绑定:

  • 依赖库ardf关于页面封装所依赖的第三方库,核心是 databinding 和 koin 库,用于数据绑定和依赖注入。
  • 基础支撑:封装工具类、扩展和事件的 Model 及接口。
  • 布局自动绑定:基于 DataBinding 封装的 BaseBindingActivity 和 BaseBindingFragment。
  • ViewModel 自动绑定:在 BaseBindingActivity 和 BaseBindingFragment 的基础上再基于 koin 实现 ViewModel 的注入与绑定。

下面将通过源码详细介绍对应功能的实现原理。

3.1 自动装载布局的实现

在 2.2 的使用介绍中可以发现,自动装载布局的实现依赖了 DataBinding,将 DataBinding 通过布局文件生成的 Binding 类作为泛型传递给了 BaseBindingActivity/ BaseBindingFragment,那么在 BaseBindingActivity/ BaseBindingFragment中是如何通过这个 Binding 类去将布局与我们的 Activity/Fragment 进行绑定的呢?

为了帮助大家更好的理解我画了一个简单的时序图:

从时序图中可以发现核心实现是在 BaseBindingActivity 的 onCreate 中,主要分为以下三步:

  • 调用 createDataBinding 创建对应布局的 Binding 类,也就是传入的泛型的实例
  • 通过 setContentView 将实例化的 Binding 对象的 root View 设置给当前 Activity
  • 调用子类实现的 initDataBinding 方法初始化界面数据

结合时序图再来看一下源码:

abstract class BaseBindingActivity<BINDING :ViewDataBinding>:AppCompatActivity() {


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        //创建 ViewDataBinding 实例
        val binding = createDataBinding()
        //绑定当前 Activity 生命周期
        binding.lifecycleOwner = this
        //设置 View
        setContentView(binding.root)

        // 初始化数据绑定
        initDataBinding(binding)
    }

    /**
     * 根据泛型 BINDING 创建 ViewDataBinding 实例
     */
    private fun createDataBinding(): BINDING {
        return getBindingType(javaClass) // 获取 ViewDataBinding 泛型实际类型
            ?.getMethod("inflate", LayoutInflater::class.java) // 反射获取 inflate 方法
            ?.invoke(null, LayoutInflater.from(this)) as BINDING // 通过反射调用 inflate 方法
    }

    /**
     * 初始化数据绑定
     * 子类实现该方法通过 binding 绑定数据
     */
    abstract fun initDataBinding(binding: BINDING)
}

代码不多,具体作用也写了相应注释,关键代码在 createDataBinding方法,做了三件事:

  • 获取当前 Activity 上 ViewDataBinding 的实际类型,即 DataBinding 通过布局文件生成的 Binding 类。
  • 通过反射获取到 ViewDataBinding 的 inflate方法,该方法会返回当前 Binding 实例。
  • 通过反射调用 inflate方法初始化 Binding 实例

getBindingType是一个全局的工具方法,源码如下:

fun getBindingType(clazz: Class<*>) : Class<*>? {
    val superclass = clazz.genericSuperclass
    if (superclass is ParameterizedType ) {
        //返回表示此类型实际类型参数的 Type 对象的数组
        val actualTypeArguments = superclass.actualTypeArguments
        return actualTypeArguments.firstOrNull {
            // 判断是 Class 类型 且是 ViewDataBinding 的子类
            it is Class<*> && ViewDataBinding::class.java.isAssignableFrom(it)
        } as? Class<*>
    }
    return null
}

实现是通过反射获取传入类型的所有泛型,然后取出第一个是 ViewDataBinding子类的类型进行返回。

这样就实现了通过泛型传入 Binding 自动加载布局并与当前 Activity 进行绑定。

BaseBindingFragment的实现逻辑与 BaseBindingActivity的实现逻辑基本一致,只是将实现换到了 onCreateView方法中,如下:

abstract class BaseBindingFragment<BINDING:ViewDataBinding>: Fragment() {
    
    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
        //创建 ViewDataBinding 实例
        val binding = createDataBinding(inflater, container)
        //绑定当前 Fragment 生命周期
        binding.lifecycleOwner = this

        // 初始化数据绑定
        initDataBinding(binding)
        //返回布局 View 对象
        return binding.root
    }

    /**
     * 根据泛型 BINDING 创建 ViewDataBinding 实例
     */
    private fun createDataBinding(inflater: LayoutInflater, container: ViewGroup?): BINDING {
        return getBindingType(javaClass)// 获取泛型类型
            ?.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java) // 反射获取 inflate 方法
            ?.invoke(null, inflater, container, false) as BINDING // 通过反射调用 inflate 方法
    }

    /**
     * 初始化数据绑定
     * 子类实现该方法通过 binding 绑定数据
     */
    abstract fun initDataBinding(binding: BINDING)

}

3.2 自动注入 ViewModel 的实现

在 MVVM 模式的开发中,一般是通过 DataBinding 将布局与 ViewModel 绑定使用,ViewModel 中的数据变化自动刷新界面,实现数据驱动 UI 刷新,那么我们怎么将这个过程进行通用的封装呢?

还是先来看一个简单的时序图:

从时序图中不难发现,核心是基于上面介绍的 BaseBindingActivity 实现的 BaseBindingViewModelActivity类,重写了 initDataBinding方法并实现了如下功能:

  • 调用 createViewModel方法创建 ViewModel 实例对象
  • 调用 Binding 的 setVariable方法绑定 ViewModel 对象

BaseBindingViewModelActivity源码如下:

open class BaseBindingViewModelActivity<BINDING : ViewDataBinding, VM : BaseViewModel>:
    BaseBindingActivity<BINDING>(){

    //创建 ViewModel 变量并延迟初始化
    val viewModel:VM by lazy {
        createViewModel()
    }

    override fun initDataBinding(binding: BINDING) {
        //绑定 viewModel
        //绑定变量为 vm。
        // 具体业务实现中在实际的布局 xml 文件中声明当前视图的 ViewModel 变量为 vm 即可自动进行绑定。
        binding.setVariable(BR.vm,viewModel)
    }

    /**
     * @description 初始化 ViewModel 并自动进行绑定
     * @return VM ViewModel 实例对象
     */
    private fun createViewModel():VM{
        try {
            //注入 ViewModel,并转换为 VM 类型
            return injectViewModel() as VM
        }catch (e:Exception){
            // 抛出异常
            throw Exception("ViewModel is not inject", e)
        }
    }
}

定义 viewModel 变量并延迟调用 createViewModel 方法进行初始化;在 initDataBinding将 viewModel 与布局的 vm变量进行绑定。

vm变量来源是因为在框架里创建了一个空的 ardf_base_activity.xml布局中定义后生成的:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>

        <variable name="vm" type="Object"/>

    </data>
  
</layout>

createViewModel方法里调用了扩展方法 injectViewModel通过 Koin 注入 ViewModel,源码如下:

@OptIn(KoinInternalApi::class)
fun ComponentActivity.injectViewModel() : ViewModel?{
    return getViewModel(javaClass, getKoinScope(), this, viewModelStore )

}

/**
 * @param javaClass Class类型
 * @param scope koin生命周期范围
 * @param owner ViewModelStoreOwner 类型,ViewModel 绑定什么周期对象,Activity、Fragment 都实现了该接口
 * @param viewModelStore 存储 ViewModel 的对象
 */
@OptIn(KoinInternalApi::class)
fun getViewModel(javaClass : Class<*>,
                 scope: Scope,
                 owner: ViewModelStoreOwner,
                 viewModelStore: ViewModelStore) : ViewModel?{
    // 获取当前 Activity 上 ViewModel 泛型的实际类型
    val viewModel = getViewModelType(javaClass)?.let {
        // 获取 ViewModelFactory
        val viewModelFactory = getViewModelFactory(owner, it.kotlin, null, null, null, scope)
        //获取注入的 ViewModel
        ViewModelLazy(it.kotlin, { viewModelStore }, { viewModelFactory} ).value
    }
    return viewModel
}

injectViewModel中调用 getViewModel方法:

  • 通过 getViewModelType获取 ViewModel 的类型
  • 调用 Koin 提供的 getViewModelFactory 获取 ViewModelFactory
  • 调用 Koin 提供的 ViewModelLazy获取注入的 ViewModel

getViewModelType 的实现跟上面的 getBindingType 的原理一样,源码如下:

fun getViewModelType(clazz: Class<*>) : Class<out ViewModel>? {
    val superclass = clazz.genericSuperclass
    if (superclass is ParameterizedType) {
        //返回表示此类型实际类型参数的 Type 对象的数组
        val actualTypeArguments = superclass.actualTypeArguments
        //返回第一个符合条件的 Type 对象
        return actualTypeArguments.firstOrNull{
            it is Class<*> && BaseViewModel::class.java.isAssignableFrom(it)
        } as? Class<out ViewModel>
    }
    return null
}

最终实现自动注入 ViewModel 并与当前 Activity / Fragment 布局进行绑定的功能。

BaseBindingViewModelFragment 的实现原理与 BaseBindingViewModelActivity 的实现原理相同,这里就不在重复贴代码,有兴趣的可以直接去看源码

3.3 事件处理的实现

ViewModel 的自动绑定实现了,那怎么实现事件的处理呢?我们知道通过 DataBinding 可以将事件传递到 ViewModel 中进行处理,那么又怎么将需要用到 Context 等特殊事件传递到 Activity / Fragment 里去处理呢?

同样的先看一个简单的时序图:

时序图解析:

  • 事件通过 Activity 传到到 View
  • Binding 里监听到事件后将事件传递到 ViewModel
  • ViewModel 中调用父类 BaseViewModelpostEvent方法将事件传递到 Activity

前面两步是由 Android 本身事件机制和 DataBinding 来完成的,第三步是 ardf实现的 BaseViewModel来完成的,源码如下:

open class BaseViewModel: ViewModel() {
    // 提示文字
    var hintText = MutableLiveData<Event<String>>()
    // 提示文字资源
    var hintTextRes = MutableLiveData<Event<Int>>()
    // 事件
    var event = MutableLiveData<Event<Int>>()
    
    protected fun postHintText(msg: String) {
        hintText.value = Event(msg)
    }

    protected fun postHintText(msgRes: Int) {
        hintTextRes.value = Event(msgRes)
    }

    protected fun postEvent(eventId: Int) {
        event.value = Event(eventId)
    }

    /**
     * 返回事件
     */
    fun back(){
        postEvent(EVENT_BACK)
    }
}

声明了三个变量:hintTexthintTextResevent分别用于传递提示文字、提示文字资源和事件,并提供了对应的 post方法用于快速调用;另外提供了一个 back方法用于传递返回事件。

所有事件都是通过一个 Event 类进行包裹,源码如下:

class Event<T>(private val value: T) {

    //是否已被处理
    private var handled = false

    /**
     * @description 防止粘性事件被多次消费,多个观察者场景下,只会被一个观察者消费
     */
    fun getValueIfNotHandled(): T? {
        return if (handled) {
            // 已处理返回 null
            null
        } else {
            // 标记为已处理
            handled = true
            value
        }
    }

    fun get(): T {
        return value
    }
}

使用 value存放传入的值并提供获取值的 get 方法,其中定义 handled变量标记事件是否已处理,通过 getValueIfNotHandled获取值时如果已处理则返回空,未处理则返回对应的值并将事件标记为已处理,以防止一个事件被多次消费,当然如果需求如此的话可以调用 get() 方法获取事件值。

在 ViewModel 中传递事件以及事件的封装完成了,那怎么将这个事件传递到 Activity / Fragment 呢?

首先为 ViewModel 扩展一个 bind 方法:

fun BaseViewModel.bind(activity: BaseBindingViewModelActivity<*,*>) {
    observe(activity, activity){
        activity.onEvent(it)
    }
}

fun  BaseViewModel.observe( owner: LifecycleOwner, context: Context?, onEvent: (Int) -> Unit){
    // 订阅提示文字变化
    hintText.observe(owner){
        val content = hintText.value?.getValueIfNotHandled()
        if (!content.isNullOrBlank()) {
            context?.toast(content)
        }
    }
    // 订阅提示文字资源变化
    hintTextRes.observe(owner) {
        val contentRes = hintTextRes.value?.getValueIfNotHandled() ?: -1
        if (contentRes > 0) {
            context?.toast(contentRes)
        }
    }

    // 订阅事件变化
    event.observe(owner) {
        event.value?.getValueIfNotHandled()?.let {
            onEvent(it)
        }
    }
}

作用是订阅 hintTexthintTextRes的变化后弹出 toast提示;同时订阅事件 event 的变化调用 onEvent方法, onEvent是接口 OnEventListener提供的方法:

interface OnEventListener {
    /**
     *
     * @description ViewModel 事件响应
     * @param eventId 事件 id,根据实际业务自定义
     * @return
     *
     */
    fun onEvent(eventId:Int)
}

BaseBindingViewModelActivity需实现 OnEventListener并在初始化 ViewModel 后调用 bind 方法,如下:

open class BaseBindingViewModelActivity<BINDING : ViewDataBinding, VM : BaseViewModel>:
    BaseBindingActivity<BINDING>(), OnEventListener {

    ...
        
    override fun  onEvent(eventId: Int) {
        if(eventId == EVENT_BACK){
            onBackPressed()
        }
    }

    /**
     * @description 初始化 ViewModel 并自动进行绑定
     * @return VM ViewModel 实例对象
     */
    private fun createViewModel():VM{
        try {
            //注入 ViewModel,并转换为 VM 类型
            val viewModel = injectViewModel() as VM
            // 订阅事件
            viewModel.bind(this)  
            return viewModel
        }catch (e:Exception){
            // 抛出异常
            throw Exception("ViewModel is not inject", e)
        }
    }
}

这样就将事件传递到了 Activity / Fragment 的 onEvent 回调方法中,在该回调中就可以自定义处理 ViewModel 中传递过来的事件。

4. 总结

本文主要介绍了 ardf(Android 快速开发框架)中基于 DataBinding + Koin 的 MVVM 模式的页面快速开发及事件处理的使用方法,并通过源码解析详细介绍了其实现原理,从而进一步提高 Android 开发的效率。

源码地址:ardf

mavenCentral:com.loongwind.ardf:base:1.0.1

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿

猜你喜欢

转载自juejin.im/post/7123901762573959175