How ViewModel survive in configuration changed

How ViewModel survive in configuration changed

In https://developer.android.com/topic/libraries/architecture/viewmodel, the first line mentioned that

The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.

Let find out how it work underhood

Lifecycle on Rotation

From the diagram above, we can see that viewmodel are persist through the destroy and recreation of the activity.

I written a small demo associate to the scenario above to validate whether it is still the same viewmodel that persist the rotation:

MainActivity
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class MainActivity : AppCompatActivity() {
val model: FirstViewModel by viewModels()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
Log.d(TAG, "onSaveInstanceState: " + this.toString())
Log.d(TAG, "onSaveInstanceState: " + model.toString())
}

override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
Log.d(TAG, "onRestoreInstanceState: " + this.toString())
Log.d(TAG, "onRestoreInstanceState: " + model.toString())
}
}

And now I will perform a rotation on my device to check the log:

Log
1
2
3
4
D/MainActivityTADA: onSaveInstanceState: me.chkfung.tada.MainActivity@bd87c29
D/MainActivityTADA: onSaveInstanceState: me.chkfung.tada.kotlin.FirstViewModel@1f721d1
D/MainActivityTADA: onRestoreInstanceState: me.chkfung.tada.MainActivity@1a37028
D/MainActivityTADA: onRestoreInstanceState: me.chkfung.tada.kotlin.FirstViewModel@1f721d1

From the Log result, we can see that it is the same viewmodel object although the activity is different already.

There come a question, from the first line of the demo code, the viewmodel is actually declared in my Activity class, how does it persist the same object when the activity itself is no longer the same?

So I started to make some wild guess:

  • Is the viewModel obtained from a static object?
    • If yes, is this static object bound to global scope or activity scope?
      • Activity Scope - But the activity is destroyed already, how does this scope work?
      • Global Scope - Seems valid, but if there are multiple activity with the same class, how do there know which viewmodel to restore? Maybe by using a timestamp Key?
    • If No, is this viewModel created from a outer layer?

Digging into the code

Initializing our viewModel

init

1
val model: FirstViewModel by viewModels()

goto viewModels()

androidx/activity/ActivityViewModelLazy.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* Returns a [Lazy] delegate to access the ComponentActivity's ViewModel, if [factoryProducer]
* is specified then [ViewModelProvider.Factory] returned by it will be used
* to create [ViewModel] first time.
*
* ```
* class MyComponentActivity : ComponentActivity() {
* val viewmodel: MyViewModel by viewmodels()
* }
* ```
*
* This property can be accessed only after the Activity is attached to the Application,
* and access prior to that will result in IllegalArgumentException.
*/
@MainThread
inline fun <reified VM : ViewModel> ComponentActivity.viewModels(
noinline factoryProducer: (() -> Factory)? = null
): Lazy<VM> {
// 1
val factoryPromise = factoryProducer ?: {
defaultViewModelProviderFactory
}
// 2
return ViewModelLazy(VM::class, { viewModelStore }, factoryPromise)
}

  1. if we have a factory for our viewModel creation, we can pass in the factory, else default
  2. In this Initialize constructor, it will create a Lazy ViewModel that will not be created until it is call using get()
    • VM:class : our ViewModel Class
    • { viewModelStore } : obtained from ComponentActivity inner classes
    • factoryPromise : our ViewModel Factory

goto ViewModelLazy

androidx/lifecycle/ViewModelProvider.kt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/**
* An implementation of [Lazy] used by [androidx.fragment.app.Fragment.viewModels] and
* [androidx.activity.ComponentActivity.viewmodels].
*
* [storeProducer] is a lambda that will be called during initialization, [VM] will be created
* in the scope of returned [ViewModelStore].
*
* [factoryProducer] is a lambda that will be called during initialization,
* returned [ViewModelProvider.Factory] will be used for creation of [VM]
*/
class ViewModelLazy<VM : ViewModel> (
private val viewModelClass: KClass<VM>,
private val storeProducer: () -> ViewModelStore,
private val factoryProducer: () -> ViewModelProvider.Factory
) : Lazy<VM> {
private var cached: VM? = null

override val value: VM
// 1
get() {
val viewModel = cached
return if (viewModel == null) {
val factory = factoryProducer()
val store = storeProducer()
// 2
ViewModelProvider(store, factory).get(viewModelClass.java).also {
cached = it
}
} else {
viewModel
}
}

override fun isInitialized() = cached != null
}
  1. get(), in this function, it will return the viewModel if it is created and stored in cached, else it will perform 3:
  2. Create the viewModel using the ViewModelProvider and assign to cached

Craetion and Obtain of ViewModel

goto ViewModelProvider(store, factory).get(viewModelClass.java)

androidx/lifecycle/ViewModelProvider.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class ViewModelProvider {
...
/**
* Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
* an activity), associated with this {@code ViewModelProvider}.
* <p>
* The created ViewModel is associated with the given scope and will be retained
* as long as the scope is alive (e.g. if it is an activity, until it is
* finished or process is killed).
*
* @param modelClass The class of the ViewModel to create an instance of it if it is not
* present.
* @param <T> The type parameter for the ViewModel.
* @return A ViewModel that is an instance of the given type {@code T}.
*/
@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull Class<T> modelClass) {
String canonicalName = modelClass.getCanonicalName();
if (canonicalName == null) {
throw new IllegalArgumentException("Local and anonymous classes can not be ViewModels");
}
// 1
return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}

/**
* Returns an existing ViewModel or creates a new one in the scope (usually, a fragment or
* an activity), associated with this {@code ViewModelProvider}.
* <p>
* The created ViewModel is associated with the given scope and will be retained
* as long as the scope is alive (e.g. if it is an activity, until it is
* finished or process is killed).
*
* @param key The key to use to identify the ViewModel.
* @param modelClass The class of the ViewModel to create an instance of it if it is not
* present.
* @param <T> The type parameter for the ViewModel.
* @return A ViewModel that is an instance of the given type {@code T}.
*/
@SuppressWarnings("unchecked")
@NonNull
@MainThread
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
// 2
ViewModel viewModel = mViewModelStore.get(key);

if (modelClass.isInstance(viewModel)) {
// 2.1
if (mFactory instanceof OnRequeryFactory) {
((OnRequeryFactory) mFactory).onRequery(viewModel);
}
// 3
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
// 4
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
} else {
viewModel = (mFactory).create(modelClass);
}
// 5
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
...
}
  1. Passing default_key + canonical class name and Class to another get function
  2. Obtain viewModel from ViewModelProvider.mViewModelStore
    • OnRequeryFactory allow us to have a callback when this function triggered again,
      androidx/lifecycle/ViewModelProvider.java
      1
      2
      3
      4
      static class OnRequeryFactory {
      void onRequery(@NonNull ViewModel viewModel) {
      }
      }
    • when this function triggered again means that ViewModelLazy being destroy by scenario like : rotation
  3. return viewModel if mViewModelStore contains it
  4. else create it
  5. put viewModel into mViewModelStore

After digging for several layer, we can see that ViewModelStore is a very important component that retain our viewModel,

Retaining ViewModelStore and ViewModel

goto { viewModelStore }

androidx/activity/ComponentActivity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
LifecycleOwner,
ViewModelStoreOwner,
HasDefaultViewModelProviderFactory,
SavedStateRegistryOwner,
OnBackPressedDispatcherOwner {

static final class NonConfigurationInstances {
Object custom;
ViewModelStore viewModelStore;
}

// Lazily recreated from NonConfigurationInstances by getViewModelStore()
private ViewModelStore mViewModelStore;

/**
* Returns the {@link ViewModelStore} associated with this activity
* <p>
* Overriding this method is no longer supported and this method will be made
* <code>final</code> in a future version of ComponentActivity.
*
* @return a {@code ViewModelStore}
* @throws IllegalStateException if called before the Activity is attached to the Application
* instance i.e., before onCreate()
*/
@NonNull
@Override
public ViewModelStore getViewModelStore() {
if (getApplication() == null) {
throw new IllegalStateException("Your activity is not yet attached to the "
+ "Application instance. You can't request ViewModel before onCreate call.");
}
// 1
if (mViewModelStore == null) {
// 2
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
// 3
// Restore the ViewModelStore from NonConfigurationInstances
mViewModelStore = nc.viewModelStore;
}
// 4
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}
}
  1. Check if mViewModelStore already lazily initialize, if yes, direct return
  2. Tried to obtain getLastNonConfigurationInstance before rotation
  3. if lastNonConfigurationInstance exist, obtain the viewModelStore in it
  4. if not exist, create a new ViewModelStore

Interesting, there is a lastNonCongigurationInstance that persist the viewModelStore! Let’s go deeper on this

goto getLastNonConfigurationInstance

android/app/Activity.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class Activity extends ContextThemeWrapper
implements LayoutInflater.Factory2,
Window.Callback, KeyEvent.Callback,
OnCreateContextMenuListener, ComponentCallbacks2,
Window.OnWindowDismissedCallback,
AutofillManager.AutofillClient, ContentCaptureManager.ContentCaptureClient {

static final class NonConfigurationInstances {
Object activity;
HashMap<String, Object> children;
FragmentManagerNonConfig fragments;
ArrayMap<String, LoaderManager> loaders;
VoiceInteractor voiceInteractor;
}
@UnsupportedAppUsage
/* package */ NonConfigurationInstances mLastNonConfigurationInstances;

/**
* Retrieve the non-configuration instance data that was previously
* returned by {@link #onRetainNonConfigurationInstance()}. This will
* be available from the initial {@link #onCreate} and
* {@link #onStart} calls to the new instance, allowing you to extract
* any useful dynamic state from the previous instance.
*
* <p>Note that the data you retrieve here should <em>only</em> be used
* as an optimization for handling configuration changes. You should always
* be able to handle getting a null pointer back, and an activity must
* still be able to restore itself to its previous state (through the
* normal {@link #onSaveInstanceState(Bundle)} mechanism) even if this
* function returns null.
*
* <p><strong>Note:</strong> For most cases you should use the {@link Fragment} API
* {@link Fragment#setRetainInstance(boolean)} instead; this is also
* available on older platforms through the Android support libraries.
*
* @return the object previously returned by {@link #onRetainNonConfigurationInstance()}
*/
@Nullable
public Object getLastNonConfigurationInstance() {
// 1
return mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.activity : null;
}

@UnsupportedAppUsage
final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window, ActivityConfigCallback activityConfigCallback, IBinder assistToken) {
attachBaseContext(context);

mFragments.attachHost(null /*parent*/);

...
// 2
mLastNonConfigurationInstances = lastNonConfigurationInstances;
...
}
}
  1. return mLastNonConfigurationInstances
  2. initialize mLastNonConfigurationInstances from Activity.attach

So now we know that mLastNonConfigurationInstances is passing through the attach function on Activity, but who is calling this function? As our knowledge, we know that ActivityThread is the one responsible for the creation of it.

goto ActivityThread.performLaunchActivity

android/app/ActivityThread.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

@UnsupportedAppUsage
final ArrayMap<IBinder, ActivityClientRecord> mActivities = new ArrayMap<>();

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public final Activity startActivityNow(Activity parent, String id,
Intent intent, ActivityInfo activityInfo, IBinder token, Bundle state,
Activity.NonConfigurationInstances lastNonConfigurationInstances, IBinder assistToken) {

// 2.1
ActivityClientRecord r = new ActivityClientRecord();
r.token = token;
r.assistToken = assistToken;
r.ident = 0;
r.intent = intent;
r.state = state;
r.parent = parent;
r.embeddedID = id;
r.activityInfo = activityInfo;
r.lastNonConfigurationInstances = lastNonConfigurationInstances;
...
// TODO(lifecycler): Can't switch to use #handleLaunchActivity() because it will try to
// call #reportSizeConfigurations(), but the server might not know anything about the
// activity if it was launched from LocalAcvitivyManager.
//2.2
return performLaunchActivity(r, null /* customIntent */);
}

/** Core implementation of activity launch. */
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {

...
// 1
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.configCallback,
r.assistToken);

...
// 3
synchronized (mResourcesManager) {
mActivities.put(r.token, r);
}
...
}
/** Activity client record, used for bookkeeping for the real {@link Activity} instance. */
public static final class ActivityClientRecord {
...
// 2
Activity.NonConfigurationInstances lastNonConfigurationInstances;
...
}


@Override
public void handleRelaunchActivity(ActivityClientRecord tmp,
PendingTransactionActions pendingActions) {
...
// 4
ActivityClientRecord r = mActivities.get(tmp.token);
...
// 5
handleRelaunchActivityInner(r, configChanges, tmp.pendingResults, tmp.pendingIntents,
pendingActions, tmp.startsNotResumed, tmp.overrideConfig, "handleRelaunchActivity");
...
}
  1. lastNonConfigurationInstances is calling activity.attach in ActivityThread.performLaunchActivity
  2. lastNonConfigurationInstances is a member of ActivityClientRecord
    • Instance of ActivityClientRecord is created in ActivityThread.startActivityNow and pass to ActivityThread.performLaunchActivity
  3. after launching it, it will store this r : AcitivityCliendRecord into ActivityThread.mActivities
  4. When activity configurationChanged / rotated, it will try to get ActivityClientRecord from a local map mActivities
  5. Passing this ActivityClientRecord that contains lastNonConfigurationInstances back to the recreated activity.

Overall Flow Diagram


Addition knowledge:

  1. ViewModelStore is a hashMap key value pair
    androidx/lifecycle/ViewModelStore.java
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    /**
    * Class to store {@code ViewModels}.
    * <p>
    * An instance of {@code ViewModelStore} must be retained through configuration changes:
    * if an owner of this {@code ViewModelStore} is destroyed and recreated due to configuration
    * changes, new instance of an owner should still have the same old instance of
    * {@code ViewModelStore}.
    * <p>
    * If an owner of this {@code ViewModelStore} is destroyed and is not going to be recreated,
    * then it should call {@link #clear()} on this {@code ViewModelStore}, so {@code ViewModels} would
    * be notified that they are no longer used.
    * <p>
    * Use {@link ViewModelStoreOwner#getViewModelStore()} to retrieve a {@code ViewModelStore} for
    * activities and fragments.
    */
    public class ViewModelStore {

    private final HashMap<String, ViewModel> mMap = new HashMap<>();

    final void put(String key, ViewModel viewModel) {
    ViewModel oldViewModel = mMap.put(key, viewModel);
    if (oldViewModel != null) {
    oldViewModel.onCleared();
    }
    }

    final ViewModel get(String key) {
    return mMap.get(key);
    }

    Set<String> keys() {
    return new HashSet<>(mMap.keySet());
    }

    /**
    * Clears internal storage and notifies ViewModels that they are no longer used.
    */
    public final void clear() {
    for (ViewModel vm : mMap.values()) {
    vm.clear();
    }
    mMap.clear();
    }
    }

Read Up

  1. https://developer.android.com/topic/libraries/architecture/viewmodel
  2. https://juejin.cn/post/6844903913045360648
  3. https://blog.mindorks.com/android-viewmodels-under-the-hood

Comments