It’s important that your activity maintains the state a user expects, even as it is rotated, shut down by the system or restarted by the user. As I just mentioned, it’s also important that you don’t clog up
onSaveInstanceState
with complex objects. You also don’t want to reload data from the database when you don’t need to. Let’s look at an example of an activity that allows you to search through your library of songs:
Example of the clean state of the activity and the state after a search
There are two general ways a user can leave an activity, and two different outcomes the user will expect:
- The first is if the user
completely closes
the activity. A user can completely close the activity if they swipe an activity off of the
recents screen
or if a user
navigates up or back
out of an activity. The assumption in these cases is that
the user has permanently navigated away from the activity, and if they ever re-open the activity, they will expect to start from a clean state
. For our song app, if a user completely closes the song search activity and later re-opens the activity, the song search box will be cleared and so will the search results.
- On the other hand, if a user rotates the phone or puts the activity in the background and then comes back to it, the user expects that the search results and song they searched for are there, exactly as before. There are a few ways the user could put the activity in the background. They could press the home button or navigate somewhere else in the app. Or they could receive a phone call or notification in the middle of looking at search results. In the end, though, the user expects when they come back to the activity, that the state is the same as they left it.
To implement this behavior in both situations, you will use local persistence, ViewModels and
onSaveInstanceState()
together. Each will store different data the activity uses:
- Local persistence
is used for storing all data you don’t want to lose if you open and close the activity.
Example:
The collection of all song objects, which could include audio files and metadata
- ViewModels
are used for storing all the data needed to display the associated UI Controller.
Example:
The results of the most recent search, the most recent search query
- onSaveInstanceState
is used for storing a small amount of data needed to easily reload activity state if the UI Controller is stopped and recreated by the system. Instead of storing complex objects here, persist the complex objects in local storage and store a unique ID for these objects in
onSaveInstanceState()
.
Example:
The most recent search query
In the song search example, here’s how different events should be handled:
When the user adds a song ?
The ViewModel will immediately delegate persisting this data locally. If this newly added song is something that should be shown in the UI, you should also update the data in ViewModel to reflect the addition of the song. Remember to do all database inserts off of the main thread.
When the user searches for a song ?
Whatever complex song data you load from the database for the UI Controller should be immediately stored in the ViewModel. You should also save the search query itself in the ViewModel.
When the activity goes into the background and the activity is stopped by the system ?
When the activity goes into the background,
onSaveInstanceState()
will be called. You should save the search query in the
onSaveInstanceState()
bundle. This small amount of data is easy to save. It’s also all the information you need to get the activity back into its current state.
When the activity is created
? There are three different ways this could happen:
- The activity is created for the first time
: In this case, you’ll have no data in the
onSaveInstanceState()
bundle and an empty ViewModel. When creating the ViewModel, you’ll pass an empty query and the ViewModel will know that there’s no data to load yet. The activity will start in a clean empty state.
- The activity is created after being stopped by the system
: The activity will have the query saved in an
onSaveInstanceState()
bundle. The activity should pass the query to the ViewModel. The ViewModel will see that it has no search results cached and will delegate loading the search results, using the given search query.
- The activity is created after a configuration change
: The activity will have the query saved in an
onSaveInstanceState()
bundle AND the ViewModel will already have the search results cached. You pass the query from the
onSaveInstanceState()
bundle to the ViewModel, which will determine that it already has loaded the necessary data and that it does
not
need to re-query the database.
This is one sane way to handle saving and restoring activity state. Depending on your activity implementation, you might not need to use
onSaveInstanceState()
at all. For example, some activities don’t open in a clean state after the user closes them. Currently, when I close and re-open Chrome on Android, it takes me back to the exact webpage I was looking at before closing it. If your activity behaves this way, you can ditch
onSaveInstanceState()
and instead persist everything locally. In the song searching example, that would mean persisting the most recent query, for example, in
Shared Preferences
.
Additionally, when you open an activity from an intent, the bundle of extras is delivered to you on both configuration changes and when the system restores an activity. If the search query were passed in as an intent extra, you could use the extras bundle instead of the
onSaveInstanceState()
bundle.
In both of these scenarios, though, you’d still use a ViewModel to avoid wasting cycles reloading data from the database during a configuration change!
Are ViewModels a replacement for Loaders?
TL;DR.
Yes, ViewModels used in conjunction with a few other classes can replace Loaders.
Loaders
are for loading data for UI Controllers. In addition, Loaders can survive configuration changes, if, for example, you rotate the device in the middle of a load. This sounds familiar!
A common use case for Loaders, in particular
CursorLoaders
, is to have the Loader observe the content of a database and keep the data the UI displays in sync. Using a CursorLoader, if a value in the database changes, the Loader will automatically trigger a reload of the data and update the UI.
ViewModels, used with other Architecture Components,
LiveData
and
Room
, can replace Loaders. The ViewModel ensures that the data can survive a configuration change. LiveData ensures that your UI can update when the data updates. Room ensures that when your database updates, your LiveData is notified.
Loaders are implemented as callbacks within your UI Controller, so an added benefit of ViewModels is they detangle your UI Controller and data loading. This makes you have fewer strong references between classes.
There are a few approaches to using ViewModels and LiveData to load data:
- In
this blog post
,
Ian Lake
outlines how you can use a ViewModel and LiveData to replace an
AsyncTaskLoader
.
- As your code gets more complex, you can consider having the actual data loading take place in a separate class. The purpose of a ViewModel class is to contain data for a UI controller such that that data survives configuration changes. Loading, persisting, and managing data are complicated functions that are outside of the scope of what a ViewModel traditionally does. The
Guide to Android App Architecture
suggests building a
repository
class.
“Repository modules are responsible for handling data operations. They provide a clean API to the rest of the app. They know where to get the data from and what API calls to make when data is updated. You can consider them as mediators between different data sources (persistent model, web service, cache, etc.).” ?
Guide to App Architecture
Conclusion and further learning
In this post, I answered a few questions about what the ViewModel class is and what it’s not. The key takeaways are:
- ViewModels are not a replacement for persistence ? persist your data like normal when it’s changed.
- ViewModels are not a replacement for
onSaveInstanceState()
because they only survive configuration change related destruction; they do not survive the OS stopping the app’s process.
onSaveInstanceState()
is not meant for complex data that require lengthy serialization/deserialization.
- To efficiently save and restore UI state, use a combination of persistence,
onSaveInstanceState()
and ViewModels. Complex data is saved in local persistence and
onSaveInstanceState()
is used to store unique identifiers to that complex data. ViewModels store the complex data in memory after it is loaded.
- In this scenario, ViewModels still retain the data when the activity is rotated or goes into the background, which is something that you can’t easily do by using purely
onSaveInstanceState()
.
- ViewModels and LiveData, used in conjunction, can replace Loaders. You can use Room to replace CursorLoader functionality.
- Repository classes are created to support a scalable architecture for loading, caching and syncing data.
Want more ViewModel-ly goodness? Check out:
The architecture components were created based on your feedback. If you have questions or comments about ViewModel or any of the architecture components, check out our
feedback page
. Questions about this series? Leave a comment!