前言
ViewModel只能在Activty和Fragment里使用吗,能不能在View里使用呢?
假如我要提供一个View ,它包含一堆数据和状态,比如一个新闻列表、时刻表等。我是否可以再这个这个自定义View 里使用ViewModel 去管理数据呢?
在View中使用ViewModel
答案是肯定的!那么我们说干就干,看看到底怎么使用。
为了确保与宿主Avtivity/Fragment发生管理和便于宿主管理,我们需要使用ViewModelProvider 去创建ViewModel ,典型的使用方法如下:
ViewModelProvider(this,).get(CustomModel::class.java)
但这时就遇到了麻烦,ViewModelStoreOwner ?去哪里弄,不仅没有ViewModelStoreOwner ?,也没有ViewModelStore 啊。当然,你也可以打破规则,什么都不管,直接创建ViewModel ,但是我并不建议你这么做。这里我讲解一下如何老老实实的按照“规则”去使用它。
首先要获取到ViewModelStoreOwner ?,有两种方法:
- 在你自定义View中实现它,并按照
ComponentActivity 的逻辑实现一遍; - 使用承载你自定义View的Activity或者Fragment的
ViewModelStoreOwner
在开始使用ViewModel之前,我们先准备一个自定义View,就弄一个简单的组合View:
class CustomView : RelativeLayout {
constructor(context: Context) : super(context) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initView(context)
}
private fun initView(context: Context) {
mViewModelStore = ViewModelStore()
LayoutInflater.from(context).inflate(R.layout.custom_layout, this, true)
}
}
布局文件:
<RelativeLayout
android:gravity="center"
xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
/>
</RelativeLayout>
就是一个继承自RelativeLayout ?的CustomView ,里面一个TextView展示一段文字。
在自定义View中实现ViewModelStoreOwner
接下来,让我们为上文中的CustomView 升级一下,为他加入ViewModelStoreOwner ?。按照ComponentActivity 里的方法,我们需要实现ViewModelStoreOwner ?接口,然后定义一个ViewModelStore 变量,并在销毁时清理掉所有的ViewModel 。实现后的代码如下:
/**
* 实现ViewModelStoreOwner接口*/
class CustomViewWithStoreOwner : RelativeLayout, ViewModelStoreOwner {
//定义ViewModelStore变量
private lateinit var mViewModelStore: ViewModelStore
constructor(context: Context) : super(context) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initView(context)
}
private fun initView(context: Context) {
mViewModelStore = ViewModelStore()
LayoutInflater.from(context).inflate(R.layout.custom_layout, this, true)
val customModel = ViewModelProvider(this).get(CustomViewModel::class.java)
customModel.data = "我是一个自定义控件啊"
findViewById<TextView>(R.id.tv).text = customModel.data;
}
override fun getViewModelStore(): ViewModelStore {
//接口方法实现
return mViewModelStore;
}
override fun onDetachedFromWindow() {
//View移除时清理所有的viewModel
viewModelStore.clear()
super.onDetachedFromWindow()
}
}
代码很简单,三两行注释就能理解
这是一个超级简化的实现方法。当然,如果有需求,也可以按照JetPackComponentActivity 中的方法去实现:你可以定义一个实现ViewModelStore ?的父类,并持有ViewModelStore ?变量。然后去继承这个父类实现你的自定义View就行了。但这个方法也有不少缺点:首先代码量变多了,其次,因为无法多继承,你的自定义View没法随心所欲的去继承其他父类了。
这种方法有个巨大的优点:自定义View销毁时,ViewModel便会立刻被销毁。但是我很不喜欢这种方法的,因为它需要每次都去实现ViewModelStoreOwner ?接口,还要去管理ViewModelStore ,确实很麻烦。
使用ViewTreeViewModelStoreOwner
怎么拿到ViewModelStoreOwner ?呢?贴心的JetPack早已想好了办法——ViewTreeViewModelStoreOwner 。并且Kotlin也提供了View 的扩展函数方便我们获取ViewModelStoreOwner ?:
public fun View.findViewTreeViewModelStoreOwner(): ViewModelStoreOwner? =
ViewTreeViewModelStoreOwner.get(this)
那么接下来,看一下我们的自定义View:
class CustomViewWithoutStoreOwner : RelativeLayout {
constructor(context: Context) : super(context) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initView(context)
}
private fun initView(context: Context) {
LayoutInflater.from(context).inflate(R.layout.custom_layout, this, true)
val customModel = findViewTreeViewModelStoreOwner()?.let {
ViewModelProvider(it).get(CustomViewModel::class.java)
}
customModel!!.data = "我是一个自定义控件啊"
findViewById<TextView>(R.id.tv).text = customModel.data;
}
}
你以为这就完了?跑起来看一下:
完美的空指针异常!!!为什么会这样呢?我们去看一下ViewTreeViewModelStoreOwner ?的源码:
public class ViewTreeViewModelStoreOwner {
private ViewTreeViewModelStoreOwner() {
// No instances
}
public static void set(@NonNull View view, @Nullable ViewModelStoreOwner viewModelStoreOwner) {
view.setTag(R.id.view_tree_view_model_store_owner, viewModelStoreOwner);
}
@Nullable
public static ViewModelStoreOwner get(@NonNull View view) {
ViewModelStoreOwner found = (ViewModelStoreOwner) view.getTag(
R.id.view_tree_view_model_store_owner);
if (found != null) return found;
ViewParent parent = view.getParent();
while (found == null && parent instanceof View) {
final View parentView = (View) parent;
found = (ViewModelStoreOwner) parentView.getTag(R.id.view_tree_view_model_store_owner);
parent = parentView.getParent();
}
return found;
}
}
代码很简单,一个set和一个get。set方法就是在View里添加一个Tag,而get方法会从View的Tag里寻找ViewModelStoreOwner ?,并且会不断的向上遍历所有的父View,直到发现ViewModelStoreOwner ?或者没有父View为止。所以,上文中我们没有set,当然get不到ViewModelStoreOwner ?,最终导致无法创建ViewModel了。
查看源码,我们发现androidx.activity.ComponentActivity 默认就实现了该方法:
@Override
public void setContentView(@LayoutRes int layoutResID) {
initViewTreeOwners();
super.setContentView(layoutResID);
}
private void initViewTreeOwners() {
// Set the view tree owners before setting the content view so that the inflation process
// and attach listeners will see them already present
ViewTreeLifecycleOwner.set(getWindow().getDecorView(), this);
ViewTreeViewModelStoreOwner.set(getWindow().getDecorView(), this);
ViewTreeSavedStateRegistryOwner.set(getWindow().getDecorView(), this);
}
所以我们只需要继承androidx.activity.ComponentActivity 或者照葫芦画瓢,重写一些我们的setContentView ?方法:
override fun setContentView(layoutResID: Int) {
ViewTreeViewModelStoreOwner.set(window.decorView,this)
super.setContentView(layoutResID)
}
在androidx.fragment.app.DialogFragment 和androidx.fragment.app.Fragment 也是用了ViewTreeViewModelStoreOwner.set方法。Fragment里的使用可以类比Activity参看这两个类里的官方实现。
如果此时你觉得就大功告成了,那么你就打错特错了——ViewTreeViewModelStoreOwner ?是通过getParent ?获取View的父类向上遍历的,如果我们的View还没有添加到View树中。我们肯定是拿不到任何东西的。所以CustomViewWithoutStoreOwner ?还需要做一下调整:
class CustomViewWithoutStoreOwner : RelativeLayout {
constructor(context: Context) : super(context) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
initView(context)
}
constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(context, attrs, defStyleAttr) {
initView(context)
}
private fun initView(context: Context) {
LayoutInflater.from(context).inflate(R.layout.custom_layout, this, true)
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()
val customModel = findViewTreeViewModelStoreOwner()?.let {
ViewModelProvider(it).get(CustomViewModel::class.java)
}
customModel!!.data = "我是一个自定义控件啊"
findViewById<TextView>(R.id.tv).text = customModel.data;
}
}
我们需要等View挂载到View Tree上之后再获取ViewModelStoreOwner 。到这里一切就大工高成了。
总结
总结一下在VIew里使用ViewModel有两种方法:
-
让View实现ViewModelStoreOwner ?,并由它自己管理。 该方法的优点在于可以将ViewModel的生命周期和View绑定在一起,但是实现相对负责。 -
通过ViewTreeViewModelStoreOwner ,使用承载View的Activity/Fragment里的ViewModelStoreOwner 。 该方法的优点在于使用简单方便,而且释放了View的职责,无需View去管理ViewModel。缺点是小坑偏多,要留意Activity的生命周期和View的创建过程。不然就是竹篮打水一场空。避免以下几点会导致空指针的异常的情况:
- Activity/Fragment或者它们的父类里没有调用
ViewTreeViewModelStoreOwner.set 方法; - View还没有挂载到树上就开始调用
ViewTreeViewModelStoreOwner.get 方法(在onAttachedToWindow 之后)。
上面两种情况都会造成findViewTreeViewModelStoreOwner 总是返回null的问题。
|