A new way to create a ViewModel, CreationExtras?

Posted by Trium918 on Sun, 06 Mar 2022 19:15:30 +0100

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.KeyDescriptions
ViewModelProvider.NewInstanceFactory.VIEW_MODEL_KEYViewModelProvider can distinguish multiple VM instances based on key, VIEW_MODEL_KEY is used to provide this key for the current VM
ViewModelProvider.AndroidViewModelFactory.APPLICATION_KEYProvide the current Application context
SavedStateHandleSupport.SAVED_STATE_REGISTRY_OWNER_KEYProvide the SavedStateRegistryOwner required to create the createSavedStateHandle
SavedStateHandleSupport.VIEW_MODEL_STORE_OWNER_KEYViewModelStoreOwner required for createSavedStateHandle
SavedStateHandleSupport.DEFAULT_ARGS_KEYBundle 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")
    }
}

Topics: Android ViewModel Lifecycle