Persisting Data through configuration changes in Android

Hey fellow Android Dev!
In this blog post, we will explore the significance of handling configuration changes in Android and delve into the reasons why it is crucial to address the various edge cases that may arise.

What is a configuration in Android?
Configuration in Android refers to a wide range of device characteristics and settings that can influence the behavior and appearance of an application on Android devices. Moreover, the configuration also determines the specific resources that an app will utilize once it's set to a particular configuration. I will explain more about what this means in later sections of this post.

Some examples of configurations are - Screen size, screen orientation, changing locale, App display size etc.

Why do you explicitly need to handle the configuration changes?
When any configuration changes occur, for example, if the screen orientation changes, the app must seamlessly adapt to the new orientation, ensuring a lossless transition where the data displayed on the screen persists. In Android, the system internally recreates the Activity whenever there is a change in device configurations. This process involves the system calling onDestroy() on the current activity and subsequently recreating the same activity in the new configuration. As a result, the UI is created with the updated configuration.

Here are some real-life scenarios that highlight the importance of handling configuration changes -

  1. Suppose a User is filling out a form and is halfway through it. The user then enters multi-window mode and opens another app. When the user returns, they expect the form to be half filled. Essentially they don't want the progress to be lost and continue from where they left off.

  2. Rotating the device or folding a foldable device invariably leads to changes in the screen size and display, especially when dealing with limited screen space. It is crucial to manage these dynamic configurations and adapt the app's UI accordingly.

Now that you are convinced that handling these scenarios is necessary, let's explore how we can achieve it. Mainly, there are three ways by which you can handle these scenarios:-

1) Local Persistence: This involves storing data in local storage, such as a database or shared preferences. It is commonly used to preserve data that you don't want to lose even if you close the app and return to it at a later time. However, it is not suitable for saving the state of the application's UI.

2) Retained Objects: View Models come to the rescue in this situation. They store the UI-related state in memory while the user is actively using the app, ensuring that the state is retained and accessible.

3) Saved Instance State: Saved instance state handles system-initiated process death and stores data that should be retrieved when the app is relaunched after being terminated. It allows for the restoration of important data and the seamless continuation of the app's previous state.

To be honest, all these methods deserve separate blog posts, I will touch upon Retained Objects in this blog.

Retained Objects - Welcome View Models!

View Models are lifecycle/state-aware components. This means that View Models are tightly bound to the scope of their respective viewModelStoreOwners (such as Activities, Fragments, Navigation Destinations, Navigation Graphs, etc.). View Models are retained in memory as long as their associated owner persists and remains active.

We will dig into how ViewModels works internally.

Need of a ViewModel - Essentially, viewModels is a class that holds different states and exposes them to UI. So an alternative to a VM could be a simple class that will hold the data/state to expose it to UI. But this will become problematic in case of navigating between activities or nav destinations. These cases will require storing data using the savedInstanceState mechanism.
So ViewModel classes are there for rescue which gives easy-to-access API for data persistence between different states.

How does ViewModel persist data when a configuration change happens?
To Create ViewModel objects, we need to create ViewModelProvider instances that in turn give the ViewModel objects.

ViewModel provider classes are created as -
val vmProviderOne = ViewModelProvider(this) // viewModelFactory is optional param
val vmProviderTwo = ViewModelProvider(this, viewModelFactory)

Let's get the viewModel object -
val viewModel = vmProviderOne.get(GetNotesViewModel::class.java)

If you observe, this viewModel itself is the same one that is persisted across configuration changes.
This .get(...) method is returning the same VM, let's dissect what's happening inside .get().

This get method calls another get method with a Key and reference to the modelClass. View Model instances are stored as Key-Value pairs in VieModelStore and this Key is used to retrieve the VM instance.
Let's see how other .get(..) function looks like -

This method checks for a ViewModel instance stored in the ViewModelStore using respective keys. If the ViewModel instance is not found, it uses the factory to create a new instance, saves it in the ViewModelStore, and returns that instance. The ViewModel instance is stored by the respective ViewModelStore owner.

Now that we have narrowed it down, ViewModelStores are responsible for holding the ViewModel instances that are associated with a specific ViewModelStoreOwner, such as an Activity or Fragment.

As viewModelStores are linked to specific Activities or Fragments, a question arises: How are these ViewModel instances retained when their respective Activity or Fragment is destroyed, for example, during the process of rotation?

The answer lies in the lifecycle-aware nature of ViewModel objects. When a configuration change occurs, such as device rotation, the ViewModel instances are not destroyed along with their associated Activity or Fragment. Instead, they are retained in memory and seamlessly reconnected to the new instance of the Activity or Fragment. This way, the ViewModel instances can persist their data and state across configuration changes, ensuring a smooth user experience and preventing data loss.

Let's dive into the reason -
ViewModelStoreOwner is an interface that contains a method getViewModelStore(). ComponentActivity extends this interface, let's check the method implementation.

In the code, we can observe that the ViewModelStore is checked for nullity. If the ViewModelStore is null, the code retrieves the last non-configurable instance using the getLastNonConfigurationInstance() method. If the last non-configurable instance is not null, the ViewModelStore is obtained from it; otherwise, a new ViewModelStore is created.

It's important to note that the NonConfigurationInstance object is passed from the previously destroyed Activity to the newly created Activity during rotations. This NonConfigurationInstance object holds the old ViewModel, ensuring its preservation and availability in the new Activity. So Internally, it was the NonConfigurationInstance object that was the player all along.

That's it about persisting configuration changes using ViewModels and how they work internally. Let me know in case you have any questions, feel free to connect over Twitter or LinkedIn!