在MVVM架构下像使用Activity/Fragment一样使用自定义View

561 阅读3分钟

前言

使用JetPack+Mvvm已经有一段时间了,每次使用自定义View都会去自定义bindingAdapter来更新自定义View视图的状态,最近重构的公司的老项目中充斥着大量的组合自定义View,并且业务逻辑比较复杂,这要是写起自定义bindingAdapter来可是相当痛苦,便突发奇想,自定义View可不可以也像使用Activity/Fragment一样,拥有自己的ViewModel,单独维护自己的业务,于是便有了下文的DataBindingCustomView

实现

从已有的Activity/Fragment的基类作为参考:在Activity/Fragment的基类中通ViewModelProvider() 对象来创建和管理 ViewModel 实例,并且它接收一个ViewModelStoreOwner 参数用于指定ViewModel 的生命周期范围,然而Activity 和 Fragment 是常见的 ViewModelStoreOwner 实现类,但是View/ViewGroup并没有实现该接口,所以在自定义VIew的基类中便不能使用ViewModelProvider对象来创建ViewModel。

但是官方提供了View.findViewTreeViewModelStoreOwner() 的扩展方法

1689671906951.png 该方法使用了递归的方式来遍历 View 树,查找与当前 View 相关联的 ViewModelStoreOwner

  1. 首先,它获取当前 View 的父 View,即通过 'parent' 属性获取。
  2. 然后,判断父 View 是否为 null。如果为 null,表示当前 View 不在 View 树中,无法找到 ViewModelStoreOwner,返回 null。
  3. 如果父 View 不为 null,就继续判断该父 View 是否实现了 ViewModelStoreOwner 接口。如果是,说明找到了与当前 View 相关联的 ViewModelStoreOwner,直接返回该父 View。
  4. 如果父 View 没有实现 ViewModelStoreOwner 接口,则将父 View 作为参数,递归调用 'findViewTreeViewModelStoreOwner()' 方法,继续向上查找。递归过程会不断向上遍历父 View,直到找到实现了 ViewModelStoreOwner 接口的对象或者遍历到根 View。
  5. 如果遍历到根 View(一般为 Activity 或 Fragment 的根布局),仍然没有找到实现 ViewModelStoreOwner 接口的对象,表示当前 View 不在与 ViewModel 相关的层级中,返回 null。

通过以上'findViewTreeViewModelStoreOwner()' 方法可以在 View 树中找到与当前 View 相关联的 ViewModelStoreOwner。这样,在自定义的 View 中,就可以通过该方法获取到对应的 ViewModelStoreOwner,并进一步使用 'ViewModelProvider' 获取具体的 ViewModel 实例。

上代码

DataBindingCustomView.kt

abstract class DataBindingCustomView(context: Context, attributeSet: AttributeSet) : ConstraintLayout(context,attributeSet) {

    private var binding : ViewDataBinding? = null

    protected abstract fun getDataBindingConfig(): DataBindingConfig
    protected abstract fun initViewModel()
    protected open fun initData(){}
    protected open fun observer() {}


    protected fun initBinding(){
        val dataBindingConfig = this.getDataBindingConfig()
        val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
        binding = DataBindingUtil.inflate(inflater,dataBindingConfig.layout,this,true)
        binding?.setVariable(dataBindingConfig.vmVariableId,dataBindingConfig.stateViewModel)
        val bindingParams = dataBindingConfig.bindingParams
        var i = 0
        run {
            val length = bindingParams.size()
            while (i < length) {
                binding!!.setVariable(bindingParams.keyAt(i), bindingParams.valueAt(i))
                ++i
            }
        }
    }


    /**
     * 在 MVVM 架构中,ViewModel 是与视图无关的业务逻辑层,它负责处理与界面交互相关的数据和逻辑。ViewModelStoreOwner 是一个接口,表示拥有 ViewModelStore 的对象,用于存储和管理 ViewModel 实例。
     * 通常,Activity 和 Fragment 是常见的 ViewModelStoreOwner 实现类。但是,在自定义的 View 中,无法直接获取到与之关联的 ViewModelStoreOwner。
     * 'findViewTreeViewModelStoreOwner()' 方法的作用就是在 View 树中查找与当前 View 相关联的 ViewModelStoreOwner。
     */
    protected open fun<T : BaseViewModel> getCustomViewViewModel(modelClass: Class<T>) : T?{
        val vm =  findViewTreeViewModelStoreOwner()?.let {
            ViewModelProvider(it)[modelClass]
        }

        return vm
    }

    /**
    * 获取LifecycleOwner对象
    */
    internal fun getLifecycleOwner() : LifecycleOwner? = findViewTreeLifecycleOwner()

    protected open fun getBinding() : ViewDataBinding?{
        return this.binding
    }


    override fun onAttachedToWindow() {
        super.onAttachedToWindow()
        initViewModel()
        initBinding()
        initData()
        observer()
    }


    override fun onDetachedFromWindow() {
        this.binding?.unbind()
        this.binding = null
        super.onDetachedFromWindow()
    }

}

DataBindingConfig.kt

class DataBindingConfig(
    val layout: Int,
    val vmVariableId: Int,
    val stateViewModel: ViewModel
) {
    private val bindingParams = SparseArray<Any>()

    fun getBindingParams(): SparseArray<Any> {
        return bindingParams
    }

    fun addBindingParam(variableId: Int, obj: Any): DataBindingConfig {
        if (bindingParams.get(variableId) == null) {
            bindingParams.put(variableId, obj)
        }
        return this
    }
}

使用

布局

<?xml version="1.0" encoding="utf-8"?>

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    tools:ignore="MissingDefaultResource">

    <data>
        <variable
            name="viewModel"
            type="com.xxx...TestCustomViewModel" />
        <variable
            name="click"
            type="com.xxx...TestCustomView.Click" />
    </data>

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <TextView
            android:text="@={viewModel.testTextField}"
            android:onClick="@{()->click.click()}"
            app:layout_constraintTop_toTopOf="parent"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"/>

    </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

继承DataBindingCustomView并在对应Activity/Fragment布局中添加即可

class TestCustomView(context: Context, attributeSet: AttributeSet) : DataBindingCustomView(context, attributeSet){

    private var viewModel : TestCustomViewModel? = null

    override fun getDataBindingConfig(): DataBindingConfig {
        return DataBindingConfig(R.layout.test_custom_view_layout,BR.viewModel,viewModel!!)
            .addBindingParam(BR.click,Click())
    }

    override fun initViewModel() {
        viewModel = getCustomViewViewModel(TestCustomViewModel::class.java)
    }

    /**
     * 点击事件
     */
    inner class Click{
        fun click(){
            toast("测试点击")
        }
    }

}

ViewModel

//此view对应的业务都可以写到这里
class TestCustomViewModel : BaseViewModel() {
    val testTextField = ObservableField<String>("测试")
    ......
}

结束

至此,一个符合MVVM架构的自定义view的基类就完成了,和Activity/Fragment在MVVM架构中的使用毫无区别。

本文只是提供在MVVM中使用自定义view的一种思路,希望对各位小伙伴的开发有所帮助