前言
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的問題, - Activity/Fragment或者它們的父類里沒有呼叫
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/395158.html
標籤:其他
上一篇:大學生必讀的100本書
