Android X-Lifecycle has recently moved to version 2.5.0 and one of the most important changes is the introduction of the Concept of CreatioinExtras. In a nutshell, CreationExtras help us get initialization parameters more elegantly when we create a ViewModel
1. The status quo
Let’s review how viewModels have been created so far
val vm : MyViewModel by viewModels()
Copy the code
We know that inside it is actually getting the VM through the ViewModelProvider. When there is no VM used ViewModelProvider. Factory to create a VM instances. The default Factory uses reflection to create instances, so the VM constructor cannot take arguments. If you want to create a VM using initialization parameters, 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
}
}
Copy the code
Then, declare the VM’s scene in an Activity or Fragment and create a custom Factory:
val vm : MyViewModel by viewModels {
MyViewModelFactory(application, "some data")}Copy the code
“Some data” may come from the Activity’s Intent or Fragment’s argements, so the code to prepare VM parameters in a real project might be much more complex. A Factory with “state” is not conducive to reuse. In order to ensure the correctness of VM creation, it is often necessary to equip each VM with its own Factory, which loses the original meaning of “Factory”. As the pages of your App became more complex, you had to build a separate Factory for each place where you needed to share a VM, with more redundant code.
In addition to the direct use of ViewModelProvider Factory, there are several other initialization, such as using SavedStateHandler etc, but no matter what way are essentially with the ViewModelProvider. Factory, Could not avoid the above Stateful Factory problem.
As to why a VM needs to be initialized at creation time, and how many initializations are currently available, you can refer to my article: Jetpack MVVM seven SINS: Loading Data in onViewCreated.
2. How does CretionExtras work?
Lifecycle 2.5.0- Alpha01 begins with the introduction of the concept of CreationExtras, which replaces the parameters required by Factory tasks for VM initialization and Factory no longer has to hold state.
We know ViewModelProvider. Factory using the create (modelClass) to create a VM, after 2.5.0 method signature has 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
Copy the code
After 2.5.0, the required initialization parameters can be obtained by Extras when creating a VM. Define Factory to look like this:
class ViewModelFactory : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>, extras: CreationExtras): T {
return when (modelClass) {
MyViewModel::class.java -> {
// Get custom parameters from extras
val params = extras[extraKey]!!
// Get the application from Extras
val application = extras[ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY]
/ / create the VM
MyViewModel(application, params)
}
// ...
else -> throw IllegalArgumentException("Unknown class $modelClass")}as T
}
}
Copy the code
A Stateless Factory can be reused better. We can use when to handle all types of VM creation in a single Factory, defining multiple uses at once.
3. CreationExtras.Key
The above code uses extras[key], of type CreationExtras.key, to get the initialization parameters.
Take a look at the definition of CreationExtras, which will be covered later in the member map
public sealed class CreationExtras {
internal valmap: 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}}Copy the code
The generic T for Key represents the type of the corresponding Value. This definition is more type-safe than Map
to get multiple types of key-value pairs. CoroutineContext is also designed in this way.
As follows, we can define a Key for String data
private val extraKey = object : CreationExtras.Key<String> {}
Copy the code
The system provides several preset keys for use:
CreationExtras.Key | Descriptions |
---|---|
ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEY | The ViewModelProvider can differentiate VM instances based on key, and VIEW_MODEL_KEY is used to provide the key for the current VM |
ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEY | Provides the current Application Context |
SavedStateHandleSupport.SAVED_STATE_REGISTRY_OWNER_KEY | Provides the SavedStateRegistryOwner required to create createSavedStateHandle |
SavedStateHandleSupport.VIEW_MODEL_STORE_OWNER_KEY | CreateSavedStateHandle Required ViewModelStoreOwner |
SavedStateHandleSupport.DEFAULT_ARGS_KEY | CreateSavedStateHandle Required Bundle |
The last three keys are related to the creation of SavedStateHandle, as described later in this article
4. How to create CreationExtras
So how do we create Extras and pass them in the create(modelClass, Extras) parameter?
We know from the definition of CreatioinExtras that it is a sealed class and therefore cannot be instantiated directly. We need to create instances using a subclass, MutableCreationExtras, which is a read-write decouple design that guarantees immutability at use.
Take a look at the implementation of mutable Configuration Extras, 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] asT? }}Copy the code
Remember the Map members in CreationExtras, which are used here. You can see from the use of initialExtras that CreationExtras can inherit content via merge, for example:
val extras = MutableCreationExtras().apply {
set(key1, 123)}val mergedExtras = MutableCreationExtras(extras).apply {
set(key2, "test")
}
mergedExtras[key1] / / = > 123
mergedExtras[key2] // => test
Copy the code
The ViewModelProvider defaultCreationExtras are also delivered via merge. Take a look at the code to get 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) }
}
Copy the code
You can see that Extras inherits a defaultCreationExtras by default
5. Default parameter DefaultCreationExtras
The defaultCreationExtras mentioned above are actually retrieved by the ViewModelProvider from the current Activity or Fragment.
In the Activity, for example, we can rewrite getDefaultViewModelCreationExtras () method, which to offer ViewModelProvider defaultCreationExtras, Finally, create(modelClass, Extras) parameters are passed in
Note: the Activity 1.5.0 alpha01 and fragments after 1.5.0 – alpha01 to rewrite getDefaultViewModelCreationExtras method. In previous versions, accessing defaultCreationExtras returned creationExtras.empty
Take a look at the default implementation of ComponentActivity:
public CreationExtras getDefaultViewModelCreationExtras(a) {
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;
}
Copy the code
There are applications, intents, and other default keys that are injected here.
When we need to initialize the VM with the Activity’s Intent, the code looks like this:
object : ViewModelProvider.Factory {
override fun <T : ViewModel> create(
modelClass: Class<T>,
extras: CreationExtras
): T {
// Use DEFAULT_ARGS_KEY to get the Bundle in the Intent
val bundle = extras[DEFAULT_ARGS_KEY]
valid = bundle? .getInt("id") ?: 0
return MyViewModel(id) as T
}
}
Copy the code
6. Support for AndroidViewModel and SavedStateHandle
As mentioned earlier, CreationExtras essentially make the Factory stateless. The various special Factory subclasses that used to exist for building viewModels with different parameter types, Such as AndroidViewModel AndroidViewModelFactory and SavedStateHandler ViewModel SavedStateViewModelFactory, etc. Will be phased out by CreationExtras.
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
}
}
Copy the code
As mentioned above, both the Application and the SavedStateHandler can be retrieved uniformly from CreationExtras.
The createSavedStateHandle() extension function creates a SavedStateHandler based on CreationExtras
public fun CreationExtras.createSavedStateHandle(a): 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
)
}
Copy the code
Required savedStateRegistryOwner also comes from CreationExtras parameters, such as, in addition, view the latest code SavedStateViewModelFactory, Its internal implementation has also been refactored based on CreationExtras as above.
7. Support for Compose
Let’s take a quick look at how CreationExtras are used in Compose.
Note that Gradle depends on the following upgrades:
- 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.Emptyval 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)
Copy the code
You can get the current defaultExtras via LocalViewModelStoreOwner and then add your own ExtrAs as needed.
8. Create a ViewModelFactory using the DSL
2.5.0- Alpha03 added a way to create a ViewModelFactory using DSL,
Note that Gradle depends on the following upgrades:
- Androidx. Lifecycle: lifecycle – viewmodel – KTX: 2.5.0 – alpha03
- Androidx. Fragments: fragments – KTX: 1.5.0 – alpha03
The use effect is as follows:
val factory = viewModelFactory {
initializer {
TestViewModel(this[key])
}
}
Copy the code
viewModelFactory{… With intializer} {… } are defined 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)
}
Copy the code
InitializerViewModelFactorBuilder used to build a InitializerViewModelFactory, later to introduce.
AddInitializer stores VM:: Class with corresponding CreationExtras.() -> VM in the Initializers list:
private val initializers = mutableListOf<ViewModelInitializer<*>>()
fun <T : ViewModel> addInitializer(clazz: KClass<T>, initializer: CreationExtras. () - >T) {
initializers.add(ViewModelInitializer(clazz.java, initializer))
}
Copy the code
Just mentioned InitializerViewModelFactor in the create, create the VM through initializers, the code is as follows:
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
}
}
returnviewModel ? :throw IllegalArgumentException(
"No initializer set for given class ${modelClass.name}")}}Copy the code
Since Initializers are a list, they can store the creation of multiple VMS, so you can configure the creation of multiple VMS using DSL:
val factory = viewModelFactory {
initializer {
MyViewModel(123)
}
initializer {
MyViewModel2("Test")}}Copy the code