CreationExtras 來了,建立 ViewModel 的新方式

語言: CN / TW / HK

theme: vuepress highlight: androidstudio


Androidx-Lifecycle 在近期邁入到了 2.5.0 版本,其中最重要的一個變化是引入了 CreatioinExtras 的概念。一句話概括 CreationExtras 的作用:幫助我們在建立 ViewModel 時更優雅地獲取初始化引數

1. 現狀的問題

先回顧一下目前為止的 ViewModel 的建立方式 kotlin val vm : MyViewModel by viewModels() 我們知道其內部其實是通過 ViewModelProvider 獲取 VM。當 VM 不存在時使用 ViewModelProvider.Factory 建立 VM 例項。預設 Factory 使用反射建立例項,所以 VM 的建構函式不能有引數 。如果希望使用初始化引數建立 VM 則需要定義自己的 Factory : ```kotlin class MyViewModelFactory( private val application: Application, private val param: String ) : ViewModelProvider.Factory {

override fun <T : ViewModel> create(modelClass: Class<T>): T {
    return MyViewModel(application, param) as T
}

} ``` 然後,在 Activity 或 Fragment 中宣告 VM 的現場,建立自定義 Factory:

kotlin val vm : MyViewModel by viewModels { MyViewModelFactory(application, "some data") }

"some data" 可能來自 Activity 的 Intent 或者 Fragment 的 argements,所以一個真實專案中為了準備 VM 引數的程式碼可能要複雜得多。一個持有“狀態”的 Factory 不利於複用,為了保證 VM 建立時的正確性,往往需要為每個 VM 都配備專屬的 Factory,失去了“工廠”原本存在的意義。隨著 App 的頁面越發複雜,每一處需要共享 VM 的地方都要單獨構建 Factory ,冗餘程式碼也越來越多。

除了直接使用 ViewModelProvider.Factory,還有其他幾種初始化方式,例如藉助 SavedStateHandler 等,但是無論何種方式本質上都是藉助了 ViewModelProvider.Factory,都免不了上述 Stateful Factory 的問題。

至於為什麼 VM 需要在建立時進行初始化,以及目前可用的幾種初始化方式,可以參考我的這篇文章:《Jetpack MVVM 七宗罪之:在 onViewCreated 中載入資料》

2. CretionExtras 是怎麼解決的?

Lifecycle 2.5.0-alpha01 開始引入了 CreationExtras 的概念,它替代了 Factory 的任務為 VM 初始化所需的引數,Factory 無需再持有狀態。

我們知道 ViewModelProvider.Factory 使用 create(modelClass) 建立 VM ,在 2.5.0 之後方法簽名發生瞭如下變化:

kotlin //before 2.5.0 fun <T : ViewModel> create(modelClass: Class<T>): T //after 2.5.0 fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T 2.5.0 之後在建立 VM 時可以通過 extras 獲取所需的初始化引數。定義 Factory 變成下面這樣:

kotlin class ViewModelFactory : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T { return when (modelClass) { MyViewModel::class.java -> { // 通過extras獲取自定義引數 val params = extras[extraKey]!! // 通過extras獲取application val application = extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] // 建立 VM MyViewModel(application, params) } // ... else -> throw IllegalArgumentException("Unknown class $modelClass") } as T } }

一個 Stateless 的 Factory 可以更好地複用。我們可以在一個 Factory 中使用 when 處理所有型別的 VM 建立,一次定義多處使用。

3. CreationExtras.Key

上面程式碼中使用 extras[key] 獲取初始化引數,key 的型別是 CreationExtras.Key

看一下 CreationExtras 的定義就明白了,成員 map 後文會介紹到

```kotlin public sealed class CreationExtras { internal val map: MutableMap<Key<*>, Any?> = mutableMapOf()

/**
 * Key for the elements of [CreationExtras]. [T] is a type of an element with this key.
 */
public interface Key<T>

/**
 * Returns an element associated with the given [key]
 */
public abstract operator fun <T> get(key: Key<T>): T?

/**
 * Empty [CreationExtras]
 */
object Empty : CreationExtras() {
    override fun <T> get(key: Key<T>): T? = null
}

} ```

Key 的泛型 T 代表對應 Value 的型別。相對於 Map<K,V> ,這種定義方式可以更加型別安全地獲取多種型別的鍵值對,CoroutineContext 等也是採用這種設計。

如下, 我們可以自定義一個 String 型別資料的 Key

kotlin private val extraKey = object : CreationExtras.Key<String> {}

系統以及提供了幾個預置的 Key 供使用:

|CreationExtras.Key| Descriptions| |--|--| |ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY|ViewModelProvider 可以基於 key 區分多個 VM 例項,VIEW_MODEL_KEY 用來提供當前 VM 的這個 key| |ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY|提供當前 Application context| |SavedStateHandleSupport.SAVED_STATE_REGISTRY_OWNER_KEY|提供建立 createSavedStateHandle 所需的 SavedStateRegistryOwner| |SavedStateHandleSupport.VIEW_MODEL_STORE_OWNER_KEY|createSavedStateHandle 所需的 ViewModelStoreOwner| |SavedStateHandleSupport.DEFAULT_ARGS_KEY|createSavedStateHandle 所需的 Bundle|

後三個 Key 都跟 SavedStateHandle 的建立有關,後文會進行介紹

4. 如何建立 CreationExtras

那麼我們如何建立 Extras 並傳入 create(modelClass, extras) 的引數中呢?

CreatioinExtras 的定義中我們知道它是一個密封類,因此無法直接例項化。我們需要使用其子類 MutableCreationExtras 來建立例項,這是一種讀寫分離的設計思想,保證了使用處的不可變性。

順便看一眼 MutableCreationExtras 的實現吧,非常簡單:

```kotlin public class MutableCreationExtras(initialExtras: CreationExtras = Empty) : CreationExtras() {

init {
    map.putAll(initialExtras.map)
}
/**
 * Associates the given [key] with [t]
 */
public operator fun <T> set(key: Key<T>, t: T) {
    map[key] = t
}

public override fun <T> get(key: Key<T>): T? {
    @Suppress("UNCHECKED_CAST")
    return map[key] as T?
}

} `` 還記得CreationExtras中的map成員嗎,這裡使用到了。從initialExtras的使用可看出來CreationExtras` 可以通 merge 實現內容的繼承,例如:

```kotlin val extras = MutableCreationExtras().apply { set(key1, 123) } val mergedExtras = MutableCreationExtras(extras).apply { set(key2, "test") }

mergedExtras[key1] // => 123 mergedExtras[key2] // => test ```

ViewModelProviderdefaultCreationExtras 也是通過 merge 實現的傳遞。看一下獲取 VM 的程式碼:

```kotlin public open operator fun get(key: String, modelClass: Class): T { val viewModel = store[key]

val extras = MutableCreationExtras(defaultCreationExtras)
extras[VIEW_MODEL_KEY] = key

return factory.create(
    modelClass,
    extras
).also { store.put(key, it) }

} `` 可以發現extras預設會繼承一個defaultCreationExtras`

5. 預設引數 DefaultCreationExtras

上面提到的 defaultCreationExtras 實際上是 ViewModelProvider 從當前 Activity 或者 Fragment 中獲取的。

以 Activity 為例,我們可以通過重寫 getDefaultViewModelCreationExtras() 方法,來提供 defaultCreationExtrasViewModelProvider,最終傳入 create(modelClass, extras) 的引數

注意: Activity 1.5.0-alpha01 和 Fragment 1.5.0-alpha01 之後才能重寫 getDefaultViewModelCreationExtras 方法。之前的版本中,訪問 defaultCreationExtras 將返回 CreationExtras.Empty

看一下 ComponentActivity 的預設實現:

java public CreationExtras getDefaultViewModelCreationExtras() { MutableCreationExtras extras = new MutableCreationExtras(); if (getApplication() != null) { extras.set(ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY, getApplication()); } extras.set(SavedStateHandleSupport.SAVED_STATE_REGISTRY_OWNER_KEY, this); extras.set(SavedStateHandleSupport.VIEW_MODEL_STORE_OWNER_KEY, this); if (getIntent() != null && getIntent().getExtras() != null) { extras.set(SavedStateHandleSupport.DEFAULT_ARGS_KEY, getIntent().getExtras()); } return extras; }

有 Application 以及 Intent 等,前面介紹的預設 Key 都是這裡注入的。

當我們需要使用 Activity 的 Intent 初始化 VM 時,程式碼如下:

kotlin object : ViewModelProvider.Factory { override fun <T : ViewModel> create( modelClass: Class<T>, extras: CreationExtras ): T { // 使用 DEFAULT_ARGS_KEY 獲取 Intent 中的 Bundle val bundle = extras[DEFAULT_ARGS_KEY] val id = bundle?.getInt("id") ?: 0 return MyViewModel(id) as T } }

6. 對 AndroidViewModel 和 SavedStateHandle 的支援

前面說了,CreationExtras 本質上就是讓 Factory 變得無狀態。以前為了構建不同引數型別的 ViewModel 而存在的各種特殊的 Factory 子類,比如 AndroidViewModelAndroidViewModelFactory 以及 SavedStateHandler ViewModelSavedStateViewModelFactory 等等,都會由於 CreationExtras 出現而逐漸退出舞臺。

kotlin class CustomFactory : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T { return when (modelClass) { HomeViewModel::class -> { // Get the Application object from extras val application = checkNotNull(extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]) // Pass it directly to HomeViewModel HomeViewModel(application) } DetailViewModel::class -> { // Create a SavedStateHandle for this ViewModel from extras val savedStateHandle = extras.createSavedStateHandle() DetailViewModel(savedStateHandle) } else -> throw IllegalArgumentException("Unknown class $modelClass") } as T } }

如上,無論 Application 還是 SavedStateHandler 都可以統一從 CreationExtras 獲取。

createSavedStateHandle() 擴充套件函式可以基於 CreationExtras 建立 SavedStateHandler

kotlin public fun CreationExtras.createSavedStateHandle(): SavedStateHandle { val savedStateRegistryOwner = this[SAVED_STATE_REGISTRY_OWNER_KEY] val viewModelStateRegistryOwner = this[VIEW_MODEL_STORE_OWNER_KEY] val defaultArgs = this[DEFAULT_ARGS_KEY] val key = this[VIEW_MODEL_KEY] return createSavedStateHandle( savedStateRegistryOwner, viewModelStateRegistryOwner, key, defaultArgs ) } 所需的 savedStateRegistryOwner 等引數也來自 CreationExtras,此外,檢視 SavedStateViewModelFactory 的最新程式碼可知,其內部實現也像上面那樣基於 CreationExtras 重構過了。

7. 對 Compose 的支援

再來簡單看看 Compose 中如何使用 CreationExtras

注意 Gradle 依賴升級如下: - androidx.activity:activity-compose:1.5.0-alpha01 - androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha01

```kotlin val owner = LocalViewModelStoreOwner.current val defaultExtras = (owner as? HasDefaultViewModelProviderFactory)?.defaultViewModelCreationExtras ?: CreationExtras.Empty

val extras = MutableCreationExtras(defaultExtras).apply { set(extraKeyId, 123) }

val factory = remember { object : ViewModelProvider.Factory { override fun create( modelClass: Class, extras: CreationExtras ): T { val id = extras[extraKeyId]!! return MainViewModel(id) as T } } }

val viewModel = factory.create(MainViewModel::class.java, extras) `` 可以通過LocalViewModelStoreOwner獲取當前的defaultExtras,然後根據需要新增自己的extras` 即可。

8. 使用 DSL 建立 ViewModelFactory

2.5.0-alpha03 新增了用 DSL 建立 ViewModelFactory 的方式,

注意 Gradle 依賴升級如下: - androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0-alpha03 - androidx.fragment:fragment-ktx:1.5.0-alpha03

使用效果如下:

kotlin val factory = viewModelFactory { initializer { TestViewModel(this[key]) } }

viewModelFactory{...}intializer{...} 的定義分別如下:

```kotlin public inline fun viewModelFactory( builder: InitializerViewModelFactoryBuilder.() -> Unit ): ViewModelProvider.Factory = InitializerViewModelFactoryBuilder().apply(builder).build()

inline fun InitializerViewModelFactoryBuilder.initializer( noinline initializer: CreationExtras.() -> VM ) { addInitializer(VM::class, initializer) } ```

InitializerViewModelFactorBuilder 用來 build 一個 InitializerViewModelFactory,稍後對其進行介紹。

addInitializerVM::class 與對應的 CreationExtras.() -> VM 存入 initializers 列表:

```kotlin private val initializers = mutableListOf<ViewModelInitializer<*>>()

fun addInitializer(clazz: KClass, initializer: CreationExtras.() -> T) { initializers.add(ViewModelInitializer(clazz.java, initializer)) } ```

剛提到的 InitializerViewModelFactorcreate 時,通過 initializers 建立 VM,程式碼如下:

```kotlin class InitializerViewModelFactory( private vararg val initializers: ViewModelInitializer<*> ) : ViewModelProvider.Factory {

override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
    var viewModel: T? = null
    @Suppress("UNCHECKED_CAST")
    initializers.forEach {
        if (it.clazz == modelClass) {
            viewModel = it.initializer.invoke(extras) as? T
        }
    }
    return viewModel ?: throw IllegalArgumentException(
        "No initializer set for given class ${modelClass.name}"
    )
}

} `` 由於initializers` 是一個列表,所以可以儲存多個 VM 的建立資訊,因此可以通過 DSL 配置多個 VM 的建立:

kotlin val factory = viewModelFactory { initializer { MyViewModel(123) } initializer { MyViewModel2("Test") } }