ViewModel

隨著 Android 架構的演進,從 MVC 到 MVP 再到現在的 MVVM,項目的結構越來越清晰,耦合度也越來越低,本質上講就是對 UI 和邏輯的分離,而在這一分離的過程中,MVP 的 presenter 和 MVVM 中的ViewModel 都起了很重要的作用,Presenter 不必多說,就是一個類封裝了我們的邏輯代碼,并加了一些回調。我們要講的是 ViewModel 如何創建使用,如何和頁面生命周期綁定以及如何在配置更改時恢復數據。

1.what?


ViewModelLiveData 是組成 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

  1. onSaveInstanceState 帶來的挑戰

我們知道 Activity 和 Fragment 通常會在下面三種情況下被銷毀:

  1. 從當前界面永久離開: 用戶導航至其他界面或直接關閉 Activity (通過點擊返回按鈕或執行的操作調用了 finish() 方法)。對應 Activity 實例被永久關閉;
  2. Activity 配置被改變: 例如,旋轉屏幕等操作,會使 Activity 需要立即重建;
  3. 應用在后臺時,其進程被系統殺死: 這種情況發生在設備剩余運行內存不足,系統又亟須釋放一些內存的時候。當進程在后臺被殺死后,用戶又返回該應用時,Activity 也需要被重建。

在后兩種情況中,我們通常都希望重建 Activity。ViewModel 會處理第二種情況,因為在這種情況下 ViewModel 沒有被銷毀;而在第三種情況下, ViewModel 被銷毀了。所以一旦出現了第三種情況,便需要在 Activity 的 onSaveInstanceState 相關回調中保存和恢復 ViewModel 中的數據。

  1. Saved State 模塊

ViewModel 保存和恢復的數據范圍僅限于配置更改導致的重建,并不支持因為資源限制導致 Activity 重建的情況。但是,大家對此的呼聲卻從來沒有停歇,Google 因此新增了一個 SavedStateHandle 類,用來滿足我們的要求。該模塊會在應用進程被殺死時恢復 ViewModel 的數據。在免除了與 Activity 繁瑣的數據交換后,ViewModel 也真正意義上的做到了管理和持有所有自己的數據。

SavedStateHandle 和 Bundle 一樣,以鍵值對形式存儲數據,它包含在 ViewModel 中,并且可以在應用處于后臺時進程被殺死的情況下幸存下來。諸如用戶 id 等需要在 onSaveInstanceState 時得到保存下來的數據,現在都可以存在 SavedStateHandle 中。

  1. 使用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個過程:

  1. 先通過 key 從 ViewModelStore (緩存)獲取 ViewModel 對象,如果緩存中存在,直接返回。Activity 經過橫屏重建之后,返回 ViewMode 的對象就是這里返回。
  2. 如果緩存不存在,那么通過 Factory 創建一個對象,然后放在緩存中,最后返回。

3.2.ViewModel 如何做到配置更改時依然可以恢復數據?

在上面講SaveState的時候,提到了Activity 和 Fragment 被銷毀的三種情況,在這三種情況下的 ViewModel 的生命周期可以看下圖:

image.png

從這張圖里面,我們可以看出,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 可以從哪里獲取呢?我們從上面的代碼中可以找到兩個地方:

  1. 從 NonConfigurationInstances 獲取。
  2. 創建一個新的 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深入提煉并應用到實際項目中吧!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。