前言
使用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() 的扩展方法
该方法使用了递归的方式来遍历 View 树,查找与当前 View 相关联的 ViewModelStoreOwner
- 首先,它获取当前 View 的父 View,即通过 'parent' 属性获取。
- 然后,判断父 View 是否为 null。如果为 null,表示当前 View 不在 View 树中,无法找到 ViewModelStoreOwner,返回 null。
- 如果父 View 不为 null,就继续判断该父 View 是否实现了 ViewModelStoreOwner 接口。如果是,说明找到了与当前 View 相关联的 ViewModelStoreOwner,直接返回该父 View。
- 如果父 View 没有实现 ViewModelStoreOwner 接口,则将父 View 作为参数,递归调用 'findViewTreeViewModelStoreOwner()' 方法,继续向上查找。递归过程会不断向上遍历父 View,直到找到实现了 ViewModelStoreOwner 接口的对象或者遍历到根 View。
- 如果遍历到根 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的一种思路,希望对各位小伙伴的开发有所帮助