Androidx-Lifecycle has recently moved to version 2.5.0, one of the most important changes being the introduction of the concept of CreatioinExtras. One sentence summarizing the role of CreationExtras: helps us gracefully get initialized parameters when creating ViewModel s
1. Status Quo
Review how ViewModel s have been created so far
val vm : MyViewModel by viewModels()
We know that the VM is actually acquired internally through the ViewModelProvider. Use ViewModelProvider when VM does not exist. Factory creates a VM instance. The default Factory creates instances using reflection, so the constructor of the VM cannot have parameters. If you need to pass in initialization parameters when the VM is created, you need to define your own Factory:
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 } }
Create this custom Factory where the VM is declared in Activity or Fragment:
val vm : MyViewModel by viewModels { MyViewModelFactory(application, "some data") }
"some data" may come from Intent in Activity or argements in Fragment, and the parameter code required to prepare the VM in a real project is often much more complex. A Factory with a "state" becomes difficult to reuse. To ensure the correctness of VM creation, each VM needs to be equipped with its own factory, which loses its meaning as a "factory". With more and more pages on App, every place that needs to share a VM needs to build a factory separately, which is more and more expensive to use.
In addition to using ViewModelProvider directly. Factory also has several other initialization methods, such as SavedStateHandler, but in any case, it is essentially ViewModelProvider. Factories are inevitably expensive to create.
Refer to this article for reasons why VM s need to be initialized when they are created and for several of the initialization methods currently available: Jetpack MVVM Three of Seven Crimes: Requesting Data in onViewCreated
2. How did CretionExtras solve it?
Lifecycle 2.5.0-alpha01 introduced the concept of CreationExtras, where the parameters required by the VM can be obtained and Factory no longer needs to hold state.
ViewModelProvider.Factory created the VM using the create(modelClass) method, and after 2.5.0 the method signature changed as follows:
//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
After 2.5.0, when we create a VM, we can inject the required initialization parameters through extras. Defining Factory becomes the following:
class ViewModelFactory : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T { return when (modelClass) { MyViewModel::class.java -> { // Getting custom parameters from extras val params = extras[extraKey]!! // Get application from extras val application = extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY] // Create VM MyViewModel(application, params) } // ... else -> throw IllegalArgumentException("Unknown class $modelClass") } as T } }
Factory of a Stateless can be better reused. We can use when in one Factory to handle all types of VM creation, defining multiple uses at a time.
3. CreationExtras.Key
The code above uses extras[key] to get the initialization parameters, and the key is of type CreationExtras.Key. Looking at the definition of CreationExtras, you can see that the member map is described later
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's generic T represents the type of the corresponding Value. Compared to Map<K, V>, this way of defining keys and values, which CoroutineContext and others use, allows more type-safe access to multiple types of key-value pairs.
Here, we can customize a Key for String-type data
private val extraKey = object : CreationExtras.Key<String> {}
Several preset Key s are also available
CreationExtras.Key | Descriptions |
---|---|
ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY | ViewModelProvider can distinguish multiple VM instances based on key, VIEW_MODEL_KEY is used to provide this key for the current VM |
ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY | Provide the current Application context |
SavedStateHandleSupport.SAVED_STATE_REGISTRY_OWNER_KEY | Provide the SavedStateRegistryOwner required to create the createSavedStateHandle |
SavedStateHandleSupport.VIEW_MODEL_STORE_OWNER_KEY | ViewModelStoreOwner required for createSavedStateHandle |
SavedStateHandleSupport.DEFAULT_ARGS_KEY | Bundle required for createSavedStateHandle |
The last three keys are related to the creation of SavedStateHandle, which is described in more detail later
4. How to create CreationExtras
So how do we create Extras and pass them in to the parameters of create(modelClass, extras)?
From the definition of CreatioinExtras, we know that it is a sealed class and therefore cannot be instantiated directly. We need to use its subclass MutableCreationExtras to create instances, which is a read-write separation design that ensures the immutability of usage.
By the way, take a look at the implementation of MutableCreationExtras, which is very simple:
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? } }
Remember the map member in CreationExtras, used here. The use of initialExtras shows that CreationExtras can inherit content through merge, for example
val extras = MutableCreationExtras().apply { set(key1, 123) } val mergedExtras = MutableCreationExtras(extras).apply { set(key2, "test") } mergedExtras[key1] // => 123 mergedExtras[key2] // => test
ViewModelProvider uses this feature to pass defaultCreationExtras to see the code that gets the VM:
public open operator fun <T : ViewModel> get(key: String, modelClass: Class<T>): T { val viewModel = store[key] val extras = MutableCreationExtras(defaultCreationExtras) extras[VIEW_MODEL_KEY] = key return factory.create( modelClass, extras ).also { store.put(key, it) } }
You can see that extras added a defaultCreationExtras by default
5. DefaultCreationExtras
The defaultCreationExtras mentioned above are actually taken from the current Activity or Fragment by the ViewModelProvider.
With Activity as an example, we can provide defaultCreationExtras to the ViewModelProvider by overriding the getDefaultViewModelCreationExtras() method, and eventually inject them into the VM by creating (modelClass, extras) parameters.
Note: The getDefaultViewModelCreationExtras method cannot be overridden until Activity 1.5.0-alpha01 and Fragment 1.5.0-alpha01. Previously, accessing defaultCreationExtras returned CreationExtras.Empty
Take a look at the default implementation of ComponentActivity:
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; }
There are Application s, Intent s, and so on, so you can see that the default Key s described earlier are injected here.
When we need to initialize the VM using Activity Intent:
object : ViewModelProvider.Factory { override fun <T : ViewModel> create( modelClass: Class<T>, extras: CreationExtras ): T { // Using DEFAULT_ARGS_KEY Gets Bundle in Intent val bundle = extras[DEFAULT_ARGS_KEY] val id = bundle?.getInt("id") ?: 0 return MyViewModel(id) as T } }
6. Support for AndroidViewModel and Saved StateHandle
As mentioned earlier, CreationExtras essentially makes Factory stateless. Various special Factory subclasses that previously existed to construct ViewModels of different parameter types, such as AndroidViewModel Factory of AndroidViewModel and Saved StateHandler ViewModel's Saved StateViewModelFactory, have gradually become meaningless as CreationExtras appear.
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 } }
As mentioned above, both Application and SavedStateHandler can be obtained in a uniform way as CreationExtras.
The createSavedStateHandle() extension function creates a SavedStateHandler based on Creation Extras
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 ) }
The required parameters, such as savedStateRegistryOwner, also come from the preset Key described earlier.
In fact, the internal logic of the latest Saved StateViewModelFactory has been refactored by CreationExtras as above.
7. Support for Compose
Let's take a brief look at how CreationExtras is used in Compose.
Note that Gradle dependency upgrades are as follows:
- androidx.activity:activity-compose:1.5.0-alpha01
- androidx.lifecycle:lifecycle-viewmodel-compose:2.5.0-alpha01
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 <T : ViewModel> create( modelClass: Class<T>, extras: CreationExtras ): T { val id = extras[extraKeyId]!! return MainViewModel(id) as T } } } val viewModel = factory.create(MainViewModel::class.java, extras)
You can get the current defaultExtras through LocalViewModelStoreOwner and add your own extras as needed.
8. Create ViewModelFactory using DSL
2.5.0-alpha03 adds a new way to create ViewModelFactory with DSL.
Note that Gradle dependency upgrades are as follows:
- androidx.lifecycle:lifecycle-viewmodel-ktx:2.5.0-alpha03
- androidx.fragment:fragment-ktx:1.5.0-alpha03
The results are as follows:
val factory = viewModelFactory { initializer { TestViewModel(this[key]) } }
viewModelFactory{...} With intializer {...} Definitions are as follows:
public inline fun viewModelFactory( builder: InitializerViewModelFactoryBuilder.() -> Unit ): ViewModelProvider.Factory = InitializerViewModelFactoryBuilder().apply(builder).build() inline fun <reified VM : ViewModel> InitializerViewModelFactoryBuilder.initializer( noinline initializer: CreationExtras.() -> VM ) { addInitializer(VM::class, initializer) }
InitializerViewModelFactorBuilder is used to build an InitializerViewModelFactory, which is described later.
addInitializer takes VM::class and the corresponding CreationExtras. () -> VM saved in initializers list:
private val initializers = mutableListOf<ViewModelInitializer<*>>() fun <T : ViewModel> addInitializer(clazz: KClass<T>, initializer: CreationExtras.() -> T) { initializers.add(ViewModelInitializer(clazz.java, initializer)) }
The InitializerViewModelFactor just mentioned creates the VM through initializers when it is created with the following code:
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}" ) } }
Since initializers are a list, you can store creation information for multiple VMs, so you can configure the creation of multiple VMs through a DSL:
val factory = viewModelFactory { initializer { MyViewModel(123) } initializer { MyViewModel2("Test") } }