ViewModel in Activity Under the Hood: How It Survives Recreation

What actually happens to ViewModel when Activity is destroyed and recreated? We break down the entire chain: ViewModelStore, NonConfigurationInstances, ActivityThread, and ActivityClientRecord. We show in detail where and how state is stored, and how Android magically restores everything during configuration changes.

20min readAndroid
Share:

Introduction

This article doesn’t cover working with ViewModel, assuming this topic is already familiar. The main focus is on how ViewModel survives configuration changes. But first — a brief introduction to ViewModel.

ViewModel is a component of the MVVM architectural pattern that was provided by Google as a primitive allowing it to survive configuration changes. Configuration changes, in turn, are states that force activity/fragment to be recreated, which is exactly the state that ViewModel can survive. Popular configurations that lead to Activity recreation:

  1. Screen orientation changes (screenOrientation): portrait/landscape
  2. Screen direction changes (layoutDirection): rtl/ltr
  3. Application language changes (locale)
  4. Font size/screen ratio changes

Of course, there’s a way to tell the system that Activity doesn’t need to be recreated when configuration changes occur. The android:configChanges flag is used in AndroidManifest.xml in the <activity/> tag to specify which configuration changes the system should not recreate Activity for, but instead pass control to the Activity.onConfigurationChanged() method.

<activity
        android:name="MainActivity"
        android:configChanges="layoutDirection|touchscreen|density|orientation|keyboard|locale|keyboardHidden|navigation|screenLayout|mcc|mnc|fontScale|uiMode|screenSize|smallestScreenSize"
/>

However, that’s not what we’re talking about now. Our goal is to understand how ViewModel manages to survive all configuration changes and preserve its state.

ViewModel Declaration

With the advent of delegates in Kotlin, developers gained the ability to significantly simplify the creation and use of components. Now declaring ViewModel using delegates looks like this:

class MainActivity : ComponentActivity() {

    private val viewModel by viewModel<MyViewModel>()
}

Without delegates, creating a ViewModel object using explicit ViewModelProvider call looks like this:

class MainActivity : ComponentActivity() {

    private lateinit var viewModel: MyViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // In older versions ViewModelProvider was part of lifecycle-viewmodel
        viewModel = ViewModelProvider(this).get(MyViewModel::class.java)

        // After adapting ViewModel for KMP and moving ViewModelProvider to lifecycle-viewmodel-android
        // you can and should use the overloaded factory method create:
        viewModel = ViewModelProvider.create(owner = this).get(MyViewModel::class)

        // Alternative way to create ViewModel (equivalent to the previous one)
        viewModel = ViewModelProvider.create(store = this.viewModelStore).get(MyViewModel::class)
    }
}

The ViewModelProvider.create method has parameters with default values, so at the bytecode level the compiler will create several overloaded versions of the method (overloads). This allows calling it with different numbers of arguments: only with store, with store and factory, or with all parameters including extras.

Jetpack ViewModel now supports Kotlin Multiplatform (KMP), which allows using it not only on Android, but also on iOS, Desktop, and Web. This became possible thanks to separation into two modules:

lifecycle-viewmodel(expected): KMP module without Android binding. lifecycle-viewmodel-android(actual): module for working with ViewModelStoreOwner and ViewModelProvider on Android.

Starting from version 2.8.0-alpha03, lifecycle-* artifacts now officially support Kotlin Multiplatform! This means that classes like ViewModel, ViewModelStore, ViewModelStoreOwner, and ViewModelProvider can now be used in common code.

Later in the article we’ll examine exactly the viewmodel:2.8.0+ version. If the sources in the version you’re currently on differ slightly, don’t worry - with the addition of KMP support, the internal structure changed a bit, but the implementation and internal logic are the same as before KMP support.

ViewModelStoreOwner?

As we can see above, we don’t manually create a ViewModel object, but only pass its class type to ViewModelProvider, which handles creating the instance itself.

Note that we also pass the parameter owner = this to the ViewModelProvider.create method. If we look at the create method sources, we can notice that it requires owner type: ViewModelStoreOwner:

public actual companion object {

    @JvmStatic
    @Suppress("MissingJvmstatic")
    public actual fun create(
        owner: ViewModelStoreOwner, // <- we're interested in this type
        factory: Factory,
        extras: CreationExtras,
    ): ViewModelProvider = ViewModelProvider(owner.viewModelStore, factory, extras)
}

If you’re wondering why the create() method can be called without passing values for the factory and extras parameters (even though they’re mandatory):

ViewModelProvider.create(owner = this)

This is because the code uses KMP (Kotlin Multiplatform). In the expect-declaration for create(), default values are already set for factory and extras, so passing them explicitly is optional.

public expect class ViewModelProvider {
    ....
    public companion object {
        public fun create(
            owner: ViewModelStoreOwner,
            factory: Factory = ViewModelProviders.getDefaultFactory(owner),
            extras: CreationExtras = ViewModelProviders.getDefaultCreationExtras(owner),
        ): ViewModelProvider

    }
    ....
}

You can see more details in the sources: ViewModelProvider.kt

Diving into ViewModelStore / Owner

So when calling the ViewModelProvider.create() method for the owner parameter, we pass this (the activity itself), and as you can guess, this means that the activity implements (inherits from) the ViewModelStoreOwner interface. Let’s look at the sources of this interface: ViewModelStoreOwner:

public interface [[[ViewModelStoreOwner |https://github.com/androidx/androidx/blob/androidx-main/lifecycle/lifecycle-viewmodel/src/commonMain/kotlin/androidx/lifecycle/ViewModelProvider.kt]]] {

/**
 * The owned [ViewModelStore]
 */
public val viewModelStore: ViewModelStore
}

ViewModelStoreOwner is an interface with a single field that represents a ViewModelStore (view models holder). Components like ComponentActivity, Fragment, NavBackStackEntry inherit from ViewModelStoreOwner.

The official documentation states:

A scope that owns ViewModelStore. A responsibility of an implementation of this interface is to retain owned ViewModelStore during the configuration changes and call ViewModelStore.clear, when this scope is going to be destroyed.

ViewModelStoreOwner Responsibilities:

  1. Store ViewModelStore during configuration changes.
  2. Clear ViewModelStore when ComponentActivity/Fragment is destroyed — in the onDestroy() state. All ViewModels that ViewModelStore holds are deleted.

We’ve determined that ViewModelStoreOwner is just an interface containing no logic of its own. It’s implemented by components like:

  • ComponentActivity (and its descendants: FragmentActivity, AppCompatActivity)
  • Fragment (and its derivatives: DialogFragment, BottomSheetDialogFragment, AppCompatDialogFragment).
  • NavBackStackEntry - Class from the Jetpack Navigation library (aka androidx navigation)

Next, we’re interested in ViewModelStore itself:

ViewModelStore is a class that internally delegates management to a Map (LinkedHashMap) collection for storing ViewModel by key:

private val map = mutableMapOf<String, ViewModel>()

By default, the full class name (including its package) is used as the key. This key is generated as follows in the sources of the ViewModelProviders utility class (not to be confused with ViewModelProvider):

private const val VIEW_MODEL_PROVIDER_DEFAULT_KEY: String = "androidx.lifecycle.ViewModelProvider.DefaultKey"

internal fun <T : ViewModel> getDefaultKey(modelClass: KClass<T>): String {
    return "$VIEW_MODEL_PROVIDER_DEFAULT_KEY:$modelClass.canonicalName"
}

Thus, for MyViewModel the key would look like: androidx.lifecycle.ViewModelProvider.DefaultKey:com.example.MyViewModel.

Since ViewModelStore is based on Map, it delegates all basic operations like put, get, keys, and clear to the internal Map (LinkedHashMap).

Accordingly, since the internal implementation of ViewModelStore relies on Map, it also delegates its put, get, key, clear methods to the internal Map(LinkedHashMap). The clear() method deserves special attention:

public open class ViewModelStore {

    private val map = mutableMapOf<String, ViewModel>()
    ...
    /**
     * Clears internal storage and notifies `ViewModel`s that they are no longer used.
     */
    public fun clear() {
        for (vm in map.values) {
            vm.clear()
        }
        map.clear()
    }
}

Let’s understand what’s happening here. When our ViewModelStoreOwner (in the form of ComponentActivity or Fragment) finally dies (death not related to recreation due to configuration changes), it calls the clear() method on ViewModelStore.

In the clear() method, the for loop goes through all values (view models) stored inside the internal HashMap and calls the internal clear() method on each ViewModel. This method, in turn, initiates the call to the onCleared() method on our ViewModel.

onCleared() is a method that we can override in our ViewModel, and it’s called only when the ViewModel is finally destroyed, when the activity or fragment also finally terminates.

public actual abstract class ViewModel {
    ...
    protected actual open fun onCleared() {} // <- onCleared method that can be overridden

    @MainThread
    internal actual fun clear() {
        impl?.clear()
        onCleared() // <- call to onCleared method
    }
}

Thus, the clear() method ensures that all resources and background tasks associated with ViewModel are properly freed before destruction. Accordingly, the viewModelStore.clear() method itself is called by ViewModelStoreOwner (in the form of ComponentActivity or Fragment). Let’s choose ComponentActivity as an example to understand how cleanup works.

Below is a code fragment from ComponentActivity that tracks its destruction and calls viewModelStore.clear():

@Suppress("LeakingThis")
lifecycle.addObserver(
    LifecycleEventObserver { _, event ->
        if (event == Lifecycle.Event.ON_DESTROY) { // <- ON_DESTROY state is the trigger
            // Clear out the available context
            contextAwareHelper.clearAvailableContext()
            // And clear the ViewModelStore
            if (!isChangingConfigurations) { // <- check if ViewModelStore can be cleared
                viewModelStore.clear()      // <- ViewModelStore cleanup
            }
            reportFullyDrawnExecutor.activityDestroyed()
        }
    }
)

In this code, an observer is added to the activity lifecycle using LifecycleEventObserver. When the activity reaches the ON_DESTROY state, a check is made to see if a configuration change is occurring (isChangingConfigurations). If the activity is truly dying permanently (and not being recreated), the viewModelStore.clear() method is called, which clears all ViewModels associated with the activity.

We can see that checking the ON_DESTROY state combined with the condition if (!isChangingConfigurations) ensures that the destruction is not due to a configuration change. Only in this case is the ViewModelStore cleared and all ViewModel instances associated with this activity are removed.

In this article we examine the internal methods of the ComponentActivity class in detail, starting from version androidx.activity:activity:1.9.0-alpha01, when it was rewritten in Kotlin.

If you have an older version of the library installed and see Java implementation — don’t worry. The logic and main methods remained the same, so all presented concepts and explanations will be relevant.

ViewModel cleanup process when activity is destroyed:
Activity destruction (not related to configuration changes) ComponentActivity.onDestroy()
ViewModelStore cleanup getViewModelStore().clear()
ViewModel notification MyViewModel.onCleared()

Now we’ve understood the cleanup and destruction process of ViewModel. Let’s move to the next stage — let’s examine in detail how ViewModel object creation happens when we pass it to ViewModelProvider:

ViewModelProvider.create(owner = this).get(MyViewModel::class)

Yes, we can clarify that ViewModelProvider.create is a function with default values. For example:

ViewModelProvider.create(owner = this).get(MyViewModel::class)

Earlier we examined one of the overloaded ViewModelProvider.create methods (function with default arguments). This is a factory method that takes at minimum ViewModelStore or ViewModelStoreOwner, creates a ViewModelProvider object, and completes its work.

Now we’re interested in the next key method — get, which takes the ViewModel class as a parameter. ViewModelProvider delegates its work to the ViewModelProviderImpl class:

public actual open class ViewModelProvider private constructor(
    private val impl: ViewModelProviderImpl,
) {
    ...
    @MainThread
    public actual operator fun <T : ViewModel> get(modelClass: KClass<T>): T =
        impl.getViewModel(modelClass) // <- call to getViewModel method belonging to ViewModelProviderImpl
}

Google developers extracted the common ViewModel creation logic into a separate ViewModelProviderImpl object. This allowed avoiding code duplication on different platforms in KMP. The reason is that expect-classes in Kotlin Multiplatform cannot contain default method implementations. If they could, the implementation would be directly inside the expect-version of ViewModelProvider, without needing to extract it to a separate object. However, due to this limitation, ViewModelProviderImpl was created, which contains the common ViewModel creation logic for all platforms.

Original comment:

Kotlin Multiplatform does not support expect class with default implementation yet, so we extracted the common logic used by all platforms to this internal class.

Sources of the getViewModel() method in ViewModelProviderImpl.kt:

internal fun <T : ViewModel> getViewModel(
    modelClass: KClass<T>,
    key: String = ViewModelProviders.getDefaultKey(modelClass),
): T {
    val viewModel = store[key] // 1. Get viewmodel from ViewModelStore if it exists
    if (modelClass.isInstance(viewModel)) {
        if (factory is ViewModelProvider.OnRequeryFactory) {
            factory.onRequery(viewModel!!)
        }
        return viewModel as T
    }

    val extras = MutableCreationExtras(extras)
    extras[ViewModelProviders.ViewModelKey] = key
    // 2. Create viewmodel and put it in ViewModelStore
    return createViewModel(factory, modelClass, extras).also { vm -> store.put(key, vm) }
}

When calling ViewModelProvider.create() under the hood, the getViewModel() method is called, which performs the following steps:

  1. Checks for the existence of a ViewModel object in ViewModelStore by the given key. If the object already exists, it’s returned.
  2. If the object is not found, a new ViewModel instance is created, which is then put in ViewModelStore for subsequent use.

Where is ViewModelStore saved?

Now that we know the complete process of creating ViewModel and placing it in ViewModelStore, a logical question arises: if all ViewModels are stored inside ViewModelStore, and ViewModelStore itself is in ComponentActivity or Fragment, which implement the ViewModelStoreOwner interface, then where and how is the ViewModelStore object itself stored?

To find the answer to the question about ViewModelStore storage, let’s look at how ComponentActivity implements the ViewModelStoreOwner interface:

override val viewModelStore: ViewModelStore
get() {
    checkNotNull(application) {
        ("Your activity is not yet attached to the " +
                "Application instance. You can't request ViewModel before onCreate call.")
    }
    ensureViewModelStore()
    return _viewModelStore!!
}

We see that the ensureViewModelStore method is called, and then the _viewModelStore field is returned.

// Lazily recreated from NonConfigurationInstances by val viewModelStore
private var _viewModelStore: ViewModelStore? = null

The _viewModelStore field has no default value, so before returning it’s initialized inside the ensureViewModelStore method:

private fun ensureViewModelStore() {
    if (_viewModelStore == null) {
        // Extract ComponentActivity#NonConfigurationInstances from Activity#getLastNonConfigurationInstance() method
        val nc = lastNonConfigurationInstance as NonConfigurationInstances?
        if (nc != null) {
            // Restore ViewModelStore from NonConfigurationInstances
            _viewModelStore = nc.viewModelStore
        }
        if (_viewModelStore == null) {
            // Create ViewModelStore if there's no saved one inside NonConfigurationInstances object
            _viewModelStore = ViewModelStore()
        }
    }
}

Here’s where it gets interesting. If the _viewModelStore field is null, first an attempt is made to get it from the getLastNonConfigurationInstance() method, which returns an object of the NonConfigurationInstances class.

If ViewModelStore is also absent there, this can mean one of two things:

  1. The activity is being created for the first time and doesn’t have a saved ViewModelStore yet.
  2. The system destroyed the application process (e.g., due to memory shortage), and then the user launched the app again, causing the ViewModelStore not to be preserved.

In either case, a new ViewModelStore instance is created.

The most non-obvious part is the call to the getLastNonConfigurationInstance() method. This method belongs to the Activity class, and the NonConfigurationInstances class, whose name alone looks intriguing, is declared in ComponentActivity:

internal class NonConfigurationInstances {
    var custom: Any? = null
    var viewModelStore: ViewModelStore? = null
}

Thus, the NonConfigurationInstances object is used to store ViewModelStore during activity configuration changes. This allows preserving the ViewModel state and restoring it after activity recreation.

The custom variable defaults to null and is practically not used, since ViewModelStore more flexibly handles all the work of preserving states for surviving configuration changes. Nevertheless, the custom variable can be used by overriding functions like onRetainCustomNonConfigurationInstance and getLastCustomNonConfigurationInstance. Before the appearance of ViewModel, many developers actively used it (in 2012) to save data when recreating activity due to configuration changes.

The viewModelStore variable has type ViewModelStore and stores a reference to our ViewModelStore object. The value in this NonConfigurationInstances#viewModelStore variable is assigned when calling the onRetainNonConfigurationInstance method, and extracted when calling getLastNonConfigurationInstance (we already encountered this method above in the ensureViewModelStore method).

Having understood the NonConfigurationInstances class, let’s find out where an object of this class is created and how a value is assigned to the viewModelStore field. For this, let’s turn to the onRetainNonConfigurationInstance and getLastNonConfigurationInstance methods, which are present in Activity and ComponentActivity. The method sources in ComponentActivity look like this:

@Suppress("deprecation")
final override fun onRetainNonConfigurationInstance(): Any? {
    // Maintain backward compatibility.
    val custom = onRetainCustomNonConfigurationInstance()
    var viewModelStore = _viewModelStore
    if (viewModelStore == null) {
        // No one called getViewModelStore(), so see if there was an existing
        // ViewModelStore from our last NonConfigurationInstance
        val nc = lastNonConfigurationInstance as NonConfigurationInstances?
        if (nc != null) {
            viewModelStore = nc.viewModelStore
        }
    }
    if (viewModelStore == null && custom == null) {
        return null
    }
    val nci = NonConfigurationInstances()
    nci.custom = custom
    nci.viewModelStore = viewModelStore
    return nci
}

The onRetainNonConfigurationInstance() method returns a NonConfigurationInstances object containing a reference to the previously created ViewModelStore.

Thus, when the activity is destroyed (e.g., when the screen is rotated), this method is called, and the ViewModelStore is saved in a NonConfigurationInstances instance. When the activity is recreated, the NonConfigurationInstances object is restored through the getLastNonConfigurationInstance() method call, and the saved ViewModelStore is extracted from it.

In the onRetainNonConfigurationInstance method, logic is implemented to get the existing ViewModelStore and Custom object (if it exists). After getting these objects, they are placed in an instance of the NonConfigurationInstances class, which is then returned from the method.

The onRetainNonConfigurationInstance method creates an object of the NonConfigurationInstances class, places the viewModelStore and custom object inside, and then returns it. The question arises: who exactly calls this method?

The calling method inside the Activity class itself (the most basic Activity from which all others inherit):

NonConfigurationInstances retainNonConfigurationInstances() {
    Object activity = onRetainNonConfigurationInstance(); // <- call to onRetainNonConfigurationInstance()

    //...code

    NonConfigurationInstances nci = new NonConfigurationInstances();
    nci.activity = activity; // <- assignment of the extracted object from onRetainNonConfigurationInstance()
    nci.children = children;
    nci.fragments = fragments;
    nci.loaders = loaders;
    if (mVoiceInteractor != null) {
        mVoiceInteractor.retainInstance();
        nci.voiceInteractor = mVoiceInteractor;
    }
    return nci;
}

As we can see, the Activity class itself calls the onRetainNonConfigurationInstance method we met earlier and saves the result in the activity field of the NonConfigurationInstances class. At the same time, we encounter the NonConfigurationInstances class again, but this time it’s declared in the Activity itself and has additional fields:

static final class NonConfigurationInstances {
    Object activity; // <- Here ComponentActivity.NonConfigurationInstances will be stored
    HashMap<String, Object> children;
    FragmentManagerNonConfig fragments;
    ArrayMap<String, LoaderManager> loaders;
    VoiceInteractor voiceInteractor;
}

To eliminate confusion:

  • The ViewModelStore object is stored inside ComponentActivity#NonConfigurationInstances.
  • The ComponentActivity#NonConfigurationInstances object itself is stored in Activity#NonConfigurationInstance.
  • This is achieved through the retainNonConfigurationInstances() method of the Activity class.

But who calls the retainNonConfigurationInstances() method and where is the final Activity#NonConfigurationInstance object stored that contains ViewModelStore?

The answer to this question lies in the ActivityThread class, which is responsible for managing activity lifecycles and their interaction with the system. This class handles creating, destroying, and recreating activities, as well as saving and restoring data during configuration changes.

The method from ActivityThread that directly calls Activity.retainNonConfigurationInstances() is called ActivityThread.performDestroyActivity().

Let’s examine its sources in the ActivityThread class:

void performDestroyActivity(ActivityClientRecord r, boolean finishing,
                            boolean getNonConfigInstance, String reason) {
    //...
    if (getNonConfigInstance) {
        try {
            // Call to Activity.retainNonConfigurationInstances()
            // and save in r.lastNonConfigurationInstances
            r.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances();
        } catch (Exception e) {
            if (!mInstrumentation.onException(r.activity, e)) {
                throw new RuntimeException("Unable to retain activity "
                        + r.intent.getComponent().toShortString() + ": " + e.toString(), e);
            }
        }
    }
    //...
}

To find the ActivityThread sources, just use the search by class name in Android Studio: ActivityThread. Or go to Android sources at one of the links:

After calling the retainNonConfigurationInstances() method, the result is saved in the lastNonConfigurationInstances field of the ActivityClientRecord object:

r.lastNonConfigurationInstances = r.activity.retainNonConfigurationInstances();

The ActivityClientRecord class represents an activity record and is used to store all information related to the actual activity instance. This is a kind of data structure for keeping track of activity during application execution.

Main fields of the ActivityClientRecord class:

  • lastNonConfigurationInstancesActivity#NonConfigurationInstance object that stores ComponentActivity#NonConfigurationInstances which stores ViewModelStore.
  • stateBundle object containing saved activity state. Yes, this is the same Bundle we get in the onCreate, onRestoreInstanceState, and onSaveInstanceState methods.
  • intentIntent object representing the activity launch intention.
  • windowWindow object associated with the activity.
  • activity — the Activity object itself.
  • parent — parent activity (if any).
  • createdConfigConfiguration object containing settings applied when creating the activity.
  • overrideConfigConfiguration object containing current activity settings.

Within this article, we’re only interested in the lastNonConfigurationInstances field, as it’s the one related to storing and restoring ViewModelStore.

Now let’s understand how the performDestroyActivity() method is called within the system call.

Call sequence:

  1. ActivityTransactionItem.execute()
  2. ActivityRelaunchItem.execute()
  3. ActivityThread.handleRelaunchActivity()
  4. ActivityThread.handleRelaunchActivityInner()
  5. ActivityThread.handleDestroyActivity()
  6. ActivityThread.performDestroyActivity()

It’s important to understand that at a higher level in this chain are classes like ClientTransactionItem, ClientTransaction, and ClientLifecycleManager, and even higher — the system itself, which manages device interaction with sensors and other components. However, we won’t go deeper into this chain, as just a couple of layers up we’ll find ourselves at the level of inter-process communication (IPC) and system work with processes.

At the top of the calls is the ActivityTransactionItem.execute() method, which starts the chain: first calls getActivityClientRecord(), and then that calls ClientTransactionHandler.getActivityClient().

public abstract class ActivityTransactionItem extends ClientTransactionItem {
    @Override
    public final void execute(ClientTransactionHandler client, IBinder token,
                              PendingTransactionActions pendingActions) {
        final ActivityClientRecord r = getActivityClientRecord(client, token); // <- Call to getActivityClientRecord

        execute(client, r, pendingActions);
    }

    @NonNull
    ActivityClientRecord getActivityClientRecord(
            @NonNull ClientTransactionHandler client, IBinder token) {
        final ActivityClientRecord r = client.getActivityClient(token); // <- getting client from ClientTransactionHandler(ActivityThread)
        ...
        return r;
    }
}

ClientTransactionHandler is an abstract class, and one of its implementations is the ActivityThread class we’ve already met.

public final class ActivityThread extends ClientTransactionHandler
        implements ActivityThreadInternal {
   ...

    @Override
    public ActivityClientRecord getActivityClient(IBinder token) {
        return mActivities.get(token); // <- Returns from Map ActivityClientRecord by key
    }
   ...
}

The ActivityRelaunchItem class inherits from ActivityTransactionItem and starts the handleRelaunchActivity method on ActivityThread:

public class ActivityRelaunchItem extends ActivityTransactionItem {

    @Override
    public void execute(@NonNull ClientTransactionHandler client, @NonNull ActivityClientRecord r,
                        @NonNull PendingTransactionActions pendingActions) {
        ...
        client.handleRelaunchActivity(mActivityClientRecord, pendingActions);
        ...
    }
}

All launched activities within our application are stored in a Map collection in the ActivityThread class object:

/**
 * Maps from activity token to local record of running activities in this process.
 * ....
 */
@UnsupportedAppUsage
final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();

Thus, we’ve finally figured out that our ViewModel is actually stored in the ActivityThread object, which is a singleton. Thanks to this, ViewModel is not destroyed when configuration changes.

Important: The ActivityThread instance is a singleton and exists throughout the entire application process lifecycle. In the handleBindApplication() method inside ActivityThread, an Application object is created, which also lives until the process terminates. This means that ActivityThread and Application are linked by a common lifecycle, except that ActivityThread appears earlier — before Application creation — and manages its initialization.

ViewModelStore Restoration

Based on what we discovered earlier, the ViewModel storage chain looks as follows:

  1. ViewModel is stored inside ViewModelStore.
  2. ViewModelStore is stored in ComponentActivity#NonConfigurationInstances.
  3. ComponentActivity#NonConfigurationInstances is stored in Activity#NonConfigurationInstances.
  4. Activity#NonConfigurationInstances is stored in ActivityClientRecord.
  5. ActivityClientRecord is stored in ActivityThread.

When Activity is recreated, its attach() method is called, one of the parameters of which is Activity#NonConfigurationInstances. This object is extracted from the ActivityClientRecord associated with the Activity.

When the Activity’s configuration changes, the system immediately restarts it to apply new parameters. At this moment, ActivityThread.java instantly extracts the ViewModelStore, which is stored in ComponentActivity#NonConfigurationInstances. This object, in turn, is located inside Activity#NonConfigurationInstances.

Then Activity#NonConfigurationInstances is saved in ActivityClientRecord, associated with the recreated Activity. Inside ActivityClientRecord there is a special field lastNonConfigurationInstances, where this object is placed. The ActivityClientRecord itself is stored in a Map collection inside ActivityThread.java, which is a singleton within the application process and can survive configuration changes.

After that, ActivityThread recreates the Activity, applying new configuration parameters. When creating it, it passes to it all saved data, including NonConfigurationInstances, which ultimately contains ViewModelStore. And ViewModelStore, in turn, stores our ViewModel.

Call Diagram for Saving and Restoring ViewModelStore

The diagram below illustrates the call chain. For simplification, some details are omitted, and redundant abstractions are removed:

Summary

In this article, we didn’t touch on the work of ViewModel as such — the focus was exclusively on why it doesn’t die when Activity is recreated, and what makes this possible at all.

We traced the entire chain: ViewModelViewModelStoreComponentActivity#NonConfigurationInstancesActivity#NonConfigurationInstancesActivityClientRecordActivityThread. It is precisely in this deep nesting that the answer lies: ViewModel survives because it is not stored in Activity directly, but in an object that the system itself passes to the new Activity during configuration changes.

The ViewModelStore itself is created either from scratch or restored via getLastNonConfigurationInstance(). It is cleared only in onDestroy(), if isChangingConfigurations == false, — that is, if the Activity really dies, and is not recreated.

Under the hood, all this is provided by ActivityThread, which saves NonConfigurationInstances in ActivityClientRecord, and then passes it to the attach() method when creating a new Activity. ActivityThread is a singleton, living as long as the process, and it is the reference point through which the entire recovery chain passes.

ViewModel survives not because someone “saves” it — but because no one kills it. As long as ActivityThread is alive, ViewModelStore is also alive.

Later we will return to ActivityThread and ActivityClientRecord again, this will be within the framework of the following articles.

Discussion

Comments