In this article, we are going to discuss the internals of ViewModel which is a part of Android Architecture Components. We will first briefly discuss the usages of ViewModel in Android and then we will go in detail about how ViewModel actually works and how it retains itself on configuration changes.
Originally published at blog.mindorks.com
According to the documentation, 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.
The ViewModel
class also helps in implementing MVVM(Model-View-ViewModel) design pattern which is also the recommended Android app architecture for Android applications by Google.
Also, there are other various advantages of using ViewModel
class provided by Android framework like:
- Handle configuration changes:
ViewModel
objects are automatically retained whenever activity is recreated due to configuration changes. - Lifecycle Awareness:
ViewModel
objects are also lifecycle-aware. They are automatically cleared when theLifecycle
they are observing gets permanently destroyed. - Data Sharing: Data can be easily shared between fragments in an activity using
ViewModels
. - Avoid memory leaks:
Viemodels
also help in avoiding memory leaks as they do not keep an instance of activity/fragment. - Kotlin-Coroutines support:
ViewModel
includes support for Kotlin-Coroutines. So, they can be easily integrated for any asynchronous processing.
How ViewModel work internally?
This is the sample project we are going to use for explaining how the viewmodel
retains itself on configuration changes.
This is a very simple application with a single activity named MainActivity
which shows a counter. The counter value is our view state kept inside the CounterViewModel
using a LiveData
object. The activity also shows the hashcode
of the current activity instance.
On any configuration change, the current activity instance is destroyed and a new instance of the activity is created which makes the hashcode
to change. But the counter value is retained as we are keeping it inside our ViewModel
and the viewmodels
are not destroyed if the activity is getting recreated due to configuration changes.
The first thought around how viewmodels
are retained might be that they are stored somewhere at the global(application) level and that’s why they are not destroyed when the activity is recreated. But this assumption is wrong. The viewmodels are stored inside the activity (or FragmentManager
in case of fragments).
So let’s discuss how this magic happens and how our viewmodel
instance is retained even if the activity is recreated.
viewModel = ViewModelProvider(this, ViewModelFactory()).get(CounterViewModel::class.java)
This is the code to get a viewmodel
instance in an activity. As we can see, we are getting an instance of ViewModelProvider
by passing two arguments, our activity instance and an instance of ViewModelFactory
. Then, we are using this ViewModelProvider
instance to get our CounterViewModel
object.
Note: In the above example, passing ViewModelFactory is redundant as our CounterViewModel does not have a parameterized constructor. In case we do not pass ViewModelFactory, ViewModelProvider uses a default view model factory.
So the creation of viewmodel involves 2 steps:
- Creation of
ViewModelProvider
- Getting the instance of
Viewmodel
fromViewModelProvider
Creation of ViewModelProvider
public ViewModelProvider(@NonNull ViewModelStoreOwner owner, @NonNull Factory factory) {
this(owner.getViewModelStore(), factory);
}
The constructor of ViewModelProvider
takes two parameters, ViewModelStoreOwner
and Factory
. In our example, the activity is the ViewModelStoreOwner
and we are passing our own custom factory. ViewModelStoreOwner
is a simple interface with a single method named getViewModelStore()
public interface ViewModelStoreOwner {
@NonNull
ViewModelStore getViewModelStore();
}
In the constructor, we are simply getting the ViewModelStore
from ViewModelStoreOwner
and passing it to the other constructor where they are just assigned to the respective class members.
public ViewModelProvider(@NonNull ViewModelStore store, @NonNull Factory factory) {
mFactory = factory;
mViewModelStore = store;
}
So now we know that our activity is the ViewModelStoreOwner
and its the responsibility of our activity to provide the ViewModelStore
.
Getting the ViewModel
from ViewModelProvider
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");
}
return get(DEFAULT_KEY + ":" + canonicalName, modelClass);
}
When we invoke get()
method on our ViewModelProvider
, it gets the canonical name of the view model class and creates a key by appending the canonical name to a DEFAULT_KEY.
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key); if (modelClass.isInstance(viewModel)) {
if (mFactory instanceof OnRequeryFactory) {
((OnRequeryFactory) mFactory).onRequery(viewModel);
}
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
} else {
viewModel = (mFactory).create(modelClass);
}
mViewModelStore.put(key, viewModel);
return (T) viewModel;
}
After creating the key from the model class, it checks the ViewModelStore
(which is provided by our activity) whether a viewmodel
instance for the given key is already present or not. If the viewmodel
is already present, it simply returns the viewmodel
instance present in ViewModelStore
and if it’s not present, the ViewModelProvider
uses the Factory
instance to create a new viewmodel
object and also stores it in ViewModelStore
.
Now we know that our activity is responsible for storing the ViewModel instances. But it’s still a mystery that how these ViewModel
instances are retained even when the activity instance is recreated on configuration change.
How ViewModel retain itself?
As we saw earlier when we created ViewModelProvider
we were passing an instance of ViewModelStoreOwner
i.e., our activity
instance.
public class ComponentActivity extends androidx.core.app.ComponentActivity implements
LifecycleOwner,
ViewModelStoreOwner,
SavedStateRegistryOwner,
OnBackPressedDispatcherOwner
Our activity implements the ViewModelStoreOwner
the interface which has a single method named getViewModelStore()
public interface ViewModelStoreOwner {
@NonNull
ViewModelStore getViewModelStore();
}
Now let’s have a look at the implementation of this method.
@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.");
} //This is true when invoked for the first time
if (mViewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
// Restore the ViewModelStore from NonConfigurationInstances
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}
Here we can see that getViewModelStore()
returns the mViewModelStore
. When our activity is recreated due to any configuration change, nc(NonConfigurationInstances)
contains the previous instance of ViewModelStore
. nc(NonConfigurationInstances)
is null when our activity is created for the first time and a new ViewModelStore
is created in this case.
//ComponentActivity.java
static final class NonConfigurationInstances {
Object custom;
ViewModelStore viewModelStore;
}//Activity.java
static final class NonConfigurationInstances {
Object activity; //NonConfigurationInstances(ComponentActivity)
HashMap<String, Object> children;
FragmentManagerNonConfig fragments;
ArrayMap<String, LoaderManager> loaders;
VoiceInteractor voiceInteractor;
}
NonConfigurationInstances(Activity.java)
is the object which is retained by the Android system even when the activity gets recreated. It has a member named activity
which is an instance of NonConfigurationInstances(ComponentActivity.java)
. This instance contains ViewModelStore
.
Note:
ViewModels
are not retained directly. Instead,ViewModelStore
is retained on configuration changes which internally maintains a map ofviewmodels
.
Let’s deep dive more into this.
//Activity.java
public Object getLastNonConfigurationInstance() {
return mLastNonConfigurationInstances != null
? mLastNonConfigurationInstances.activity : null;
}//ComponentActivity.java
public final Object onRetainNonConfigurationInstance() {
Object custom = onRetainCustomNonConfigurationInstance(); ViewModelStore viewModelStore = mViewModelStore;
if (viewModelStore == null) {
// No one called getViewModelStore(), so see if there was an existing
// ViewModelStore from our last NonConfigurationInstance
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
viewModelStore = nc.viewModelStore;
}
} if (viewModelStore == null && custom == null) {
return null;
} NonConfigurationInstances nci = new NonConfigurationInstances();
nci.custom = custom;
nci.viewModelStore = viewModelStore;
return nci;
}
Whenever our activity is getting recreated due to any configuration change, onRetainNonConfigurationInstance()
gets invoked which returns the NonConfigurationInstances(ComponentActivity.java)
instance. This object is retained by the Android system so that it can be delivered to the next activity instance on recreation.
Similarly, we can also retain our own custom objects by implementing the onRetainCustomNonConfigurationInstance()
.
After the recreation of our activity, NonConfigurationInstances(Activity.java)
is received in the attach()
method of the Activity
class.
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)
This is how the viewmodels
are retained on configuration changes.
You can also connect with me on LinkedIn, Twitter, Facebook, and Github.
Thank You!!!