Saving UI States

The way in which you do, or do not, preserve UI state is a crucial part of the user experience. Whether the user rotates the device, the user restarts the app, or the system shuts down the app, it is important that your activity maintain the state a user expects.

In cases where the UI data to preserve is simple and lightweight, you might use onSaveInstanceState() alone to preserve your state data. In cases where you have complex data that you want to preserve, you can use a combination of ViewModel objects, the onSaveInstanceState() method, and persistent local storage.

This page discusses each of these approaches.

Manging simpler cases: onSaveInstanceState()

The onSaveInstanceState() callback is designed to store relatively small amounts of data needed to easily reload the state of UI controller, such as an activity or a fragment, if the system stops and later recreates that controller. This callback is meant to handle two situations:

As both of these cases imply, onSaveInstanceState() is invoked in situations in which the activity is stopped, but not finished, by the system. For example, if the user leaves the app for several hours, and the system ejects the relevant process from memory, the system calls the default implementation of onSaveInstanceState() to save each UI controller that has an ID. Later, when the user returns to the app, the system restores the activity from the saved state.

Note: onSaveInstanceState() is not called when the user explicitly closes the activity or in other cases when finish()is called.

The system automatically saves and restores a lot of UI data for you: The default implementation of onSaveInstanceState() saves information about the state of the activity’s view hierarchy, such as the text in an EditText widget or the scroll position of a ListView widget. You can also save custom data into this bundle by overriding the onSaveInstanceState() callback. If you override this method to save additional information not captured by each individual entity, you should call the default implementation unless you are prepared to save the states of every entity yourself.

onSaveInstanceState() is not designed to store large amounts of data, such as bitmaps, or complex data structures that require lengthy serialization or deserialization. Serialization can consume lots of memory if the objects being serialized are complicated. Because this process happens on the main thread during a configuration change, serialization can cause dropped frames and visual stutter if it takes too long. Therefore, instead of using onSaveInstanceState() for complex data structures, make sure to store such structures in local persistent storage; it's a good idea to store the data as soon as it's created to minimize the chance of losing it. Then, use onSaveInstanceState() to store unique IDs for each of these objects.

The next section of this document provides more details about preserving more complex data.

Managing more complex states: divide and conquer

When you have more complex data structures that you need to preserve when an activity ends, you can efficiently save and restore UI state by dividing the work among several types of storage mechanisms.

There are two general ways a user can leave an activity, leading to two different outcomes the user may expect:

  • The user completely closes the activity. A user can completely close the activity if they swipe the activity off of the Recents screen, navigate up from the activity, or back out of the 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.

  • The user rotates the phone or puts the activity in the background and then comes back to it. For example, the user performs a search and then presses the home button or answers a phone call. When they return to the search activity, they expect to find the search keyword and results still there, exactly as before.

To implement the behavior for complex data structures in either situation, you use local persistence, the ViewModel class, and the onSaveInstanceState() method together. Each of these approaches stores a different type of data used in the activity.

  • Local persistence: Stores all data you don’t want to lose if you open and close the activity.
    • Example: A collection of song objects, which could include audio files and metadata.
  • ViewModel: Stores in memory all the data needed to display the associated UI Controller.
    • Example: The song objects of the most recent search and the most recent search query.
  • onSaveInstanceState(): Stores a small amount of data needed to easily reload activity state if the system stops and then recreates the UI Controller. Instead of storing complex objects here, persist the complex objects in local storage and store a unique ID for these objects in onSaveInstanceState().
    • Example: Storing the most recent search query.

As an example, consider an activity that allows you to search through your library of songs. Here’s how different events should be handled:

When the user adds a song , the ViewModel immediately delegates 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 the ViewModel object 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 object. You should also save the search query itself in the ViewModel object.

When the activity goes into the background, the system calls onSaveInstanceState(). 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.

Restoring complex states: reassembling the pieces

When it's time for the user to return to the activity, there are two possible scenarios for recreating the activity:

  • The activity is recreated after having been stopped by the system. The activity has the query saved in an onSaveInstanceState() bundle, and should pass the query to the ViewModel. The ViewModel sees that it has no search results cached, and delegates loading the search results, using the given search query.
  • The activity is created after a configuration change. The activity has the query saved in an onSaveInstanceState() bundle, and the ViewModel already has the search results cached. You pass the query from the onSaveInstanceState() bundle to the ViewModel, which determines that it already has loaded the necessary data and that it does not need to re-query the database.

Note: When an activity is initially created, the onSaveInstanceState() bundle contains no data, and the ViewModel object is empty. When you create the ViewModel object, you pass an empty query, which tells the ViewModel object that there’s no data to load yet. Therefore, the activity starts in an empty state.

Depending on your activity implementation, you might not need to use onSaveInstanceState() at all. For example, a browser might take the user back to the exact webpage they were looking at before they exited the browser. If your activity behaves this way, you can forego using onSaveInstanceState() and instead persist everything locally. In the song-searching example, that might mean persisting the most recent query in Shared Preferences.

Additionally, when you open an activity from an intent, the bundle of extras is delivered to the activity both when the configuration changes and when the system restores the activity. If the search query were passed in as an intent extra, you could use the extras bundle instead of the onSaveInstanceState() bundle.

In either of these scenarios, you’d still use a ViewModel to avoid wasting cycles reloading data from the database during a configuration change.