隨著 Android 架構的演進,從 MVC 到 MVP 再到現在的 MVVM,項目的結構越來越清晰,耦合度也越來越低,本質上講就是對 UI 和邏輯的分離,而在這一分離的過程中,MVP 的 presenter 和 MVVM 中的ViewModel 都起了很重要的作用,Presenter 不必多說,就是一個類封裝了我們的邏輯代碼,并加了一些回調。我們要講的是 ViewModel 如何創建使用,如何和頁面生命周期綁定以及如何在配置更改時恢復數據。
1.what?
ViewModel 和 LiveData 是組成 Jetpack 的一部分,在 MVVM 架構中充當著相當重要的角色。
ViewModel 旨在以注重生命周期的方式存儲和管理界面相關的數據,讓數據可在發生屏幕旋轉等配置更改后繼續留存,所以 ViewModel 在 MVVM 中擔當的是一個數據持有者的角色,為 Activity 、Fragment 存儲數據,在配置更改的時候恢復數據,其次因為 ViewModel 存儲了數據,所以 ViewModel 可以在當前 Activity的 Fragment 中實現數據共享。
LiveData 作為ViewModel的好基友,是一個可以感知 Activity 、Fragment生命周期的數據容器。當 LiveData所持有的數據改變時,它會通知相應的組件進行更新。同時,LiveData 持有界面代碼 Lifecycle的引用,這意味著它會在界面代碼(LifecycleOwner)的生命周期處于 started 或 resumed 時作出相應更新,而在 LifecycleOwner 被銷毀時停止更新。它的優點:不用手動控制生命周期,不用擔心內存泄露,數據變化時會收到通知.
2.How?
2.1 基本用法
我們先看看 ViewModel 是怎么使用的(雖然大家都比較熟悉)。首先,我們創建一個ViewModel子類,類里面有一個 LiveData 對象:
class MyViewModel : ViewModel() {
val mNameLiveData = MutableLiveData()
}
然后我們在 Activity 里面使用它:
class MainActivity : AppCompatActivity(R.layout.activity_main) {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val textView = findViewById<TextView>(R.id.textView)
val viewModel by viewModel<MyViewModel>()
viewModel.mNameLiveData.observe(this, Observer {
textView.text = it
})
}
}
例子非常簡單,這里就不過多的介紹。需要提一句的是,在最新的ViewModel中,以前通過ViewModelProviders.of 方法來獲取 ViewModel 已經廢棄了,現在我們是通過 ViewModelProvider Factory 創建 ViewModel 對象,因此需要往 ViewModelProider 構造方法里面傳遞一個工廠類對象,如下:
class MyViewModelFactory : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
return modelClass.getConstructor().newInstance()
}
}
當然,我們可以不帶 Factory 對象。那么加入 Factory 對象之后,相比較于以前有什么好處呢?加了 Factory 之后,我們可以定義構造方法帶參的 ViewModel。比如說,我們的一個 ViewModel 構造方法需要帶一個 id 參數,那么我們可以在 Factory 的 create 方法里面創建對象直接帶進去。
我們還可以根據提供的參數使用 lazyMap 或類似的 lazy init。當參數是字符串或其他不可變類時,很容易將它們用作映射的鍵,以獲取與提供的參數相對應的 LiveData。
class Books(val names: List<String>)
data class Parameters(val namePrefix: String = "")/*只為示范*/
class GetBooksCase {
fun loadBooks(parameters: Parameters, onLoad: (Books) -> Unit) { /* Implementation detail */
}
}
class BooksViewModel(val getBooksCase: GetBooksCase) : ViewModel() {
private val booksLiveData: Map<Parameters, LiveData<Books>> = lazyMap { parameters ->
val liveData = MutableLiveData<Books>()
getBooksCase.loadBooks(parameters) {
liveData.value = it
}
return@lazyMap liveData
}
fun books(parameters: Parameters): LiveData<Books> = booksLiveData.getValue(parameters)
}
fun <K, V> lazyMap(initializer: (K) -> V): Map<K, V> {
val map = mutableMapOf<K, V>()
return map.withDefault { key ->
val newValue = initializer(key)
map[key] = newValue
return@withDefault newValue
}
}
在上面使用 lazy map 的時候,我們只使用 map 來傳遞參數,但在許多情況下,ViewModel 的一個實例將始終具有相同的參數。這時候最好將參數傳遞給構造函數,并在構造函數中使用 lazy load 或 start load。
class BooksViewModel(val getBooksCase: GetBooksCase, parameters: Parameters) : ViewModel() {
private val booksLiveData: LiveData<Books> by lazy {
val liveData = MutableLiveData<Books>()
getBooksCase.loadBooks(parameters) {
liveData.value = it
}
return@lazy liveData
}
fun books(parameters: Parameters): LiveData<Books> = booksLiveData
}
class BooksViewModelFactory(val getBooksCase: GetBooksCase, val parameters: Parameters) :
ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return BooksViewModel(getBooksCase, parameters) as T
}
}
切記,我們不要自己創建 ViewModel 對象,因為自己創建的對象不能保存因為配置更改導致 Activity 重建的數據,從而完美避開了 ViewModel 的優點。
2.2 DataBinding 中使用 ViewModel 和 LiveData
ViewModel、LiveData 與 DataBinding 并不是什么新功能,但非常好用(但因為一些 DataBinding 出了問題全局報錯不好定位的原因,被眾大佬詬病甚至棄用)。ViewModel 通常都包含一些 LiveData,而 LiveData 意味著可以被監聽。在 XML 布局文件中使用ViewModel時,調用 binding.setLifecycleOwner(this) 方法,然后將 ViewModel 傳遞給 binding 對象,就可以將 LiveData 與 Data Binding 結合起來:
class MainActivity : AppCompatActivity() {
private val myViewModel: MyViewModel by lazy {
ViewModelProvider(
this,
MyViewModelFactory()
)[MyViewModel::class.java]
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding: MainActivityBinding = DataBindingUtil.setContentView(this, R.layout.main_activity)
binding.lifecycleOwner = this
// 將 ViewModel 傳遞給 binding
binding.viewmodel = myViewModel
}
}
XML 布局文件中使用 ViewModel:
<layout>
<data>
<variable
name="viewModel"
type="com.gxj.test.MyViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@{viewModel.text}"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</layout>
注意,這里的 viewModel.text 可以是 String 類型,也可以是 LiveData。如果它是 LiveData,那么 UI 將根據 LiveData 值的改變自動刷新。
2.3 ViewMode 與 Kotlin 協程: viewModelScope
通常情況下,我們使用回調 (Callback) 處理異步調用,這種方式在邏輯比較復雜時,會導致回調嵌套地獄,代碼也變得難以理解。而協程同樣適用于處理異步調用,它能夠讓邏輯變得簡單的同時,也確保了操作不會阻塞主線程。一段簡單的協程代碼,真實情景下不要使用:
GlobalScope.launch {
longRunningFunction()
longRunningFunction1()
}
這段代碼只啟動了一個協程,但我們在真實的使用環境下很容易創建出許多協程,這就難免會導致有些協程的狀態無法被跟蹤。如果這些協程中剛好有您想要停止的任務時,就會導致任務泄漏。而為了防止任務泄漏,需要將協程加入到一個 CoroutineScope 中,它可以持續跟蹤協程的執行,也可以被取消。當 CoroutineScope 被取消時,它所跟蹤的所有協程都會被取消。上面的代碼中,我使用了GlobalScope,正如我們不推薦隨意使用全局變量一樣,這種方式通常不推薦使用。所以,如果想要使用協程,要么限定一個作用域 (scope),要么獲得一個作用域的訪問權限。而在 ViewModel 中,我們可以使用 viewModelScope 來管理協程的作用域,它是一個ViewModel 的 kotlin 擴展屬性,當 ViewModel 被銷毀時,通常都會有一些與其相關的操作也應當被停止。
舉個栗子,當我們要加載一個文件的時候: 既要做到不能在執行時阻塞主線程,又要求在退出相關界面時停止加載。當使用協程進行耗時操作時,就應當使用 viewModelScope, ,它能在 ViewModel 銷毀時 (onCleared()方法調用時) 退出。這樣我們就可以在 ViewModel 的 viewModelScope 中啟動各種協程,而不用擔心任務泄漏。
示例如下:
class MyViewModel() : ViewModel() {
fun initialize() {
viewModelScope.launch {
processLoadFile()
}
}
suspend fun processLoadFile() = withContext(Dispatchers.Default) {
// 在這里做耗時操作
}
}
2.4 ViewModel 的 Saved State
- onSaveInstanceState 帶來的挑戰
我們知道 Activity 和 Fragment 通常會在下面三種情況下被銷毀:
- 從當前界面永久離開: 用戶導航至其他界面或直接關閉 Activity (通過點擊返回按鈕或執行的操作調用了 finish() 方法)。對應 Activity 實例被永久關閉;
- Activity 配置被改變: 例如,旋轉屏幕等操作,會使 Activity 需要立即重建;
- 應用在后臺時,其進程被系統殺死: 這種情況發生在設備剩余運行內存不足,系統又亟須釋放一些內存的時候。當進程在后臺被殺死后,用戶又返回該應用時,Activity 也需要被重建。
在后兩種情況中,我們通常都希望重建 Activity。ViewModel 會處理第二種情況,因為在這種情況下 ViewModel 沒有被銷毀;而在第三種情況下, ViewModel 被銷毀了。所以一旦出現了第三種情況,便需要在 Activity 的 onSaveInstanceState 相關回調中保存和恢復 ViewModel 中的數據。
- Saved State 模塊
ViewModel 保存和恢復的數據范圍僅限于配置更改導致的重建,并不支持因為資源限制導致 Activity 重建的情況。但是,大家對此的呼聲卻從來沒有停歇,Google 因此新增了一個 SavedStateHandle 類,用來滿足我們的要求。該模塊會在應用進程被殺死時恢復 ViewModel 的數據。在免除了與 Activity 繁瑣的數據交換后,ViewModel 也真正意義上的做到了管理和持有所有自己的數據。
SavedStateHandle 和 Bundle 一樣,以鍵值對形式存儲數據,它包含在 ViewModel 中,并且可以在應用處于后臺時進程被殺死的情況下幸存下來。諸如用戶 id 等需要在 onSaveInstanceState 時得到保存下來的數據,現在都可以存在 SavedStateHandle 中。
- 使用Save State模塊
-
添加依賴
SaveStateHandle 目前在一個獨立的模塊中,所以需要在依賴中添加:
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0"
- 修改調用 ViewModelProvider 的方式
創建一個 SaveStateHandle 的ViewModel,在 onCreate() 方法中將 ViewModelProvider 的調用修改為:
class MainActivity : AppCompatActivity(R.layout.activity_main) {
val viewModel = ViewModelProvider(
this,
SavedStateViewModelFactory(application, this)
).get(MyViewModel::class.java)
}
創建 ViewModel 的類是 ViewModelFactory,而創建包含 SaveStateHandle 的 ViewModel 的工廠類是 SavedStateViewModelFactory。通過此工廠創建的 ViewModel 將持有一個基于傳入 Activity 或 Fragment 的 SaveStateHandle。如果我們的 ViewModel 構造方法只帶一個 SavedStateHandle 參數或者帶有一個Application 參數和 SavedStateHandle 參數,可以直接使用 SavedStateViewModelFactory。如果構造方法還帶有其他的參數,此時需要繼承 AbstractSavedStateViewModelFactory 實現我們自己的工廠類。在使用AbstractSavedStateViewModelFactory 時,我們需要注意一點:create 方法帶的 SavedStateHandle 參數一定傳遞到 ViewModel 里面去。
- 調用SaveStateHandle
舉一個保存用戶 ID 的例:
class MyViewModel(state :SavedStateHandle) :ViewModel() {
// 將Key聲明為常量
companion object {
private val USER_KEY = "userId"
}
private val savedStateHandle = state
fun saveCurrentUser(userId: String) {
// 存儲 userId 對應的數據
savedStateHandle.set(USER_KEY, userId)
}
fun getCurrentUser(): String {
// 從 saveStateHandle 中取出當前 userId
return savedStateHandle.get(USER_KEY)?: ""
}
}
保存: saveNewUser 方法展示了使用鍵值對的形式保存 USER_KEY 和 userId 到 SaveStateHandle 的例子。每當數據更新時,要保存新的數據到 SavedStateHandle;
獲取: 調用 savedStateHandle.get(USER_KEY) 方法獲取被保存的 userId。
現在,無論是第二還是第三種情況下,SavedStateHandle 都可以恢復界面數據。
3.why?
3.1 ViewModel 是如何創建的?
ViewModelProivder 有很多構造方法,不過最終都調到同一個地方:
public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
mFactory = factory;
mViewModelStore = store;
}
這個方法中,mFactory 就是我們預期的工廠類,用來創建 ViewModel 對象;mViewModelStore 是一個什么東西呢?這個很好理解,mViewModelStore 就是用來存儲的 ViewModel 對象的,比如同一個 Activity 的onCreate() 方法可能會多次回調,我們在 onCreate()方法初始化ViewModel,但是不可能每次 onCreate() 回調都會創建新的 ViewModel 對象,所以需要有一個東西用來存儲的我們之前創建過的 ViewModel,這個就是ViewModelStore 的作用。而 ViewModel 生命周期比 Activity 的生命周期長也是因為這個類。
那么 mViewModelStore 對象是從哪里傳過來,我們清楚的記得構造方法里面我們并沒有傳這個變量。
public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
this(owner.getViewModelStore(), factory);
}
我們可以看到從 ViewModelStoreOwner 獲取的,代碼如下
public interface ViewModelStoreOwner {
@NonNull
ViewModelStore getViewModelStore();
}
ViewModelStoreOwner是一個接口,那么哪些類是這個借口的實現類呢?如你所料,我們熟悉的ComponentActivity 和 Fragment 都實現了這個接口。
我們再來看一下 get 方法,因為真正獲取 ViewModel 對象就是通過這個方法的。
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
String canonicalName = modelClass.getCanonicalName();
if (canonicalName == null) {
throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
}
return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}
這個get方法沒有做什么事情,構造了一個默認的 key,然后調用另一個 get 方法。代碼如下:
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
} else {
viewModel = (mFactory).create(modelClass);
}
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
這個 get 方法總的來說,主要分為以下2個過程:
- 先通過 key 從 ViewModelStore (緩存)獲取 ViewModel 對象,如果緩存中存在,直接返回。Activity 經過橫屏重建之后,返回 ViewMode 的對象就是這里返回。
- 如果緩存不存在,那么通過 Factory 創建一個對象,然后放在緩存中,最后返回。
3.2.ViewModel 如何做到配置更改時依然可以恢復數據?
在上面講SaveState的時候,提到了Activity 和 Fragment 被銷毀的三種情況,在這三種情況下的 ViewModel 的生命周期可以看下圖:
從這張圖里面,我們可以看出,ViewModel 的生命周期要比Activity長一點。ViewModel 存在的時間范圍是獲取 ViewModel 時傳遞給 ViewModelProvider 的 Lifecycle。在此期間ViewModel 將一直留在內存中,直到限定其存在時間范圍的 Lifecycle 永久消失:對于 Activity,是在 Activity 完成時;而對于 Fragment,是在 Fragment 分離時。
在前面的概述中,我們已經知道 ViewModel 的生命周期要比 Activity 長一點。那 ViewModel 是怎么做到的呢?對于這個問題,我猜大家首先想到的是緩存,并且這個緩存是被 static 關鍵字修飾的。正常來說,這個實現方案是沒有問題的,我們也能找到具體的例子,比如 Eventbus 就是這么實現的。
那么在 ViewModel 中,這個是怎么實現的呢?我們都知道 ViewModel 是從一個 ViewModelStore 緩存里面的獲取,我們看了 ViewModelStore 的源碼,發現它的內部并沒有通過靜態緩存實現。那么它是怎么實現Activity 在 onDestroy 之后(重建),還繼續保留已有的對象呢?
這個我們可以從 ComponentActivity 的 getViewModelStore 方法去尋找答案:
public ViewModelStore getViewModelStore() {
if (getApplication() == null) {
throw new IllegalStateException("Your activity is not yet attached to the "
+ "Application instance. You can't request ViewModel before onCreate call.");
}
if (mViewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
// Restore the ViewModelStore from NonConfigurationInstances
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}
getViewModeStrore 方法的目的很簡單,就是獲取一個 ViewModelStrore 對象。那么這個 ViewModelStore 可以從哪里獲取呢?我們從上面的代碼中可以找到兩個地方:
- 從 NonConfigurationInstances 獲取。
- 創建一個新的 ViewModelStore 對象。
第二點我們不用看,關鍵是 NonConfigurationInstances。NonConfigurationInstances 這是什么東西?
NonConfigurationInstances 其實就是一個 Wrapper,用來包裝一下因為不受配置更改影響的數據,包括我們非常熟悉的 Fragment,比如說,一個 Activity 上面有一個 Fragment,旋轉了屏幕導致 Activity 重新創建,此時Activity 跟之前的不是同一個對象,但是 Fragment 卻是同一個,這就是通過 NonConfigurationInstances 實現的。也就是說在 getViewModelStore 方法里面,從 NonConfigurationInstances 獲取的 ViewModelStore 對象其實就是上一個 Activity 的。同時,我們還可以在 ComponentActivity 里面看到一段代碼:
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_STOP) {
Window window = getWindow();
final View decor = window != null ? window.peekDecorView() : null;
if (decor != null) {
decor.cancelPendingInputEvents();
}
}
}
});
從上面的代碼中,我們可以到如果 Activity 是因為配置更改導致 onDestroy 方法的回調,并不會清空ViewModelStore 里面的內容,這就能保證當 Activity 因為配置更改導致重建重新創建的 ViewModel 對象跟之前創建的對象是同一個。反之,如果 Activity 是正常銷毀的話,則不會保存之前創建的 ViewModel 對象,對應的是 ViewModelStore 的 clear 方法調用。其實這個 clear 方法還跟 kotlin 里面的協程有關,這里就不過多解釋了,有興趣的同學可以看看 ViewModel.viewModelScope。
現在我們來看一下 NonConfigurationInstances 為啥能保證 Activity 重建前后,ViewModeStore 是同一個對象呢?我們直接從ActivityThread的performDestroyActivity方法去尋找答案。我們知道,performDestroyActivity 方法最后會回調到 Activity 的 onDestroy 方法,我們可以通過這個方法可以找到ActivtyThread 在 Activity onDestroy 之前做了保存操作。
ActivityClientRecord performDestroyActivity(IBinder token, boolean finishing,
int configChanges, boolean getNonConfigInstance, String reason) {
// ······
performPauseActivityIfNeeded(r, "destroy");
// Activity的onStop方法回調
if (!r.stopped) {
callActivityOnStop(r, false /* saveState */, "destroy");
}
if (getNonConfigInstance) {
// ······
// retainNonConfigurationInstances方法的作用就是創建一個對象
r.lastNonConfigurationInstances= r.activity.retainNonConfigurationInstances();
// ······
}
// ······
// Activity的onDestroy方法回調
mInstrumentation.callActivityOnDestroy(r.activity);
// ······
return r;
}
從上面的代碼中看出,在 Activity 的 onStop 和 onDestroy之間,會回調 retainNonConfigurationInstances方法,同時記錄到ActivityClientRecord中去。這里retainNonConfigurationInstances 方法返回的對象就是我們之前看到的 NonConfigurationInstances 對象。
那么又在哪里恢復已保存的 NonConfigurationInstances 對象呢?這個可以從 performLaunchActivity 方法找到答案。performLaunchActivity 方法的作用就是啟動一個 Activity,Activity 重建肯定會調用這個方法。在performLaunchActivity方法里面,調用了Activity的attach方法,在這個方法,Google將已有的NonConfigurationInstances 賦值給了新的 Activity 對象。
到這里,我們就知道為啥 NonConfigurationInstances 能保證 ViewModelStore 在 Activity 重建前后是同一個對象,同時也知道為啥 ViewModel 的生命周期比 Activity 的生命周期要長一點。
總結
在本篇文章中我講述了什么是 ViewModel,如何傳遞參數到 ViewModel 中去,以及 ViewModel一些使用場景,也相信大家對 ViewModel 都能立馬上手了。接著我們又從源碼的角度分析了 ViewModel 是如何創建的,是如何和 Activity 的生命周期綁定在一起的,這讓我們能夠更深入的理解 ViewModel,最后講述了 ViewModel 在配置更改以及銷毀重建時是如何保存和恢復數據的。ViewModel 作為數據的處理和分發者,在 MVVM 盛行的當下承扮演著越來越重要的角色,讓我們把ViewModel深入提煉并應用到實際項目中吧!