Android SharedPreferences Tutorial and Example

Retaining data across configuration changes with ViewModel in Android

Configuration changes happen on Android more often than developers expect. The screen rotates, the user toggles dark mode, the font scale is increased, or the language is switched. Each event causes the running Activity to be destroyed and rebuilt, and any field held inside is wiped. For an app used by commuters on Melbourne trams, ferry passengers in Sydney harbour, or remote workers in Perth, that loss is felt within seconds of tilting the device.

The ViewModel component, part of Android Jetpack, exists to solve this problem. It is designed to outlive the Activity across configuration changes while still being cleared when the user actually leaves the screen. The Australian Android community, including groups such as GDG Sydney and the Brisbane Mobile Devs meetup, has made ViewModel a default part of new project templates for good reason: it removes a whole class of state-loss bugs that used to plague production apps.

The pattern is simple, but a few details matter: the right scope, the correct factory, the interaction with coroutines, and the boundary between in-memory and on-disk persistence. The sections below walk through the practical steps, including how to combine ViewModel with LiveData and when to escalate to onSaveInstanceState for process death scenarios.

What configuration changes mean for your Activity

A configuration change in Android occurs when the system recreates the running Activity because of a resource change. The most common trigger is screen rotation, but others include switching to dark mode, adjusting font size, plugging in an external keyboard, or changing the device language. Each of these causes the Activity to go through onPause, onStop, onDestroy and then be rebuilt from scratch.

From the user's perspective the app looks the same, but any field or list held in the Activity is gone unless explicitly preserved. Imagine someone in Adelaide scrolling through bus times and rotating the phone; without retention, the list snaps back to the top. That moment is often enough to lose user trust in the application.

It is important to distinguish configuration changes from process death. The former keeps the process alive, so a well-designed ViewModel can persist. The latter wipes everything and is handled differently, usually with saved instance state or persistent storage. Recognising which scenario you are in shapes the right tool for the job.

How the ViewModel component solves data loss

ViewModel is designed to outlive the Activity across configuration changes while remaining tied to the user's navigation flow. When the Activity is destroyed and recreated, the same ViewModel instance is reused, so any field stored inside it is still there when the new Activity binds to it.

This is achieved through the ViewModelStore, which is owned by the Activity and persists across recreation. As long as the user does not finish the Activity, the ViewModel is not cleared. A search query typed in Brisbane can therefore survive a phone rotation on the train back to the Gold Coast.

Because the ViewModel survives, you can use it as a home for any data that is expensive to reload or that the user expects to remain in place. Lists, counters, form drafts and coroutine scopes all fit naturally here.

Adding ViewModel dependencies to your project

To use ViewModel, add the lifecycle library to your module-level build.gradle. Most modern Android Studio templates already include it, but older projects may need to add it manually. The dependency is small and well-supported in Australia where most developers pull from Maven Central without regional issues.

You then instantiate the ViewModel using ViewModelProvider, passing the Activity or Fragment as the owner. From Android Studio 4.1 onwards, the IDE also offers a template-generated ViewModel class with a corresponding factory when you create a new Fragment, which speeds up setup considerably.

For projects already using dependency injection, you can wire ViewModels through a factory such as ViewModelFactory or a Hilt module. The Telstra-backed developer programmes in Australia have published several open-source examples that show this pattern in production code.

Approach Survives configuration change Survives process death Best suited for
ViewModel alone Yes No Session state, fetched lists
onSaveInstanceState Yes Yes Small primitives, Parcelable objects
SharedPreferences No, must reload Yes User settings, feature flags
Room database No, must reload Yes Structured records that must persist
SavedStateHandle inside ViewModel Yes Yes Combining ViewModel with saved state

Storing LiveData and other state in a ViewModel

MutableLiveData inside a ViewModel is a common pairing because LiveData is lifecycle-aware and the ViewModel gives it a safe home. The Activity observes the LiveData and the ViewModel updates the value, with no risk of leaking the Activity context.

For JSON-based APIs, you might receive a parsed object inside the ViewModel and expose it to the UI. If you are still working on the parsing layer, the guide on parsing JSON with Gson explains deserialisation clearly.

When the data is asynchronous, launch a coroutine inside the ViewModel using viewModelScope. The scope is automatically cancelled when the ViewModel is cleared, which prevents the classic crash of updating the UI after the Activity is gone.

Handling data persistence beyond the ViewModel

ViewModel does not survive process death, so anything that must outlive a low-memory kill belongs in onSaveInstanceState, SharedPreferences, Room or a remote source. Treat the ViewModel as a buffer for the current session, not a permanent store.

For small values such as a selected filter or a form draft, onSaveInstanceState is sufficient and complements the ViewModel. For larger objects, Parcelable works inside a Bundle, and Room is the typical choice for structured records. Australian apps needing offline-first behaviour for regional areas with patchy coverage, such as parts of the Northern Territory, often combine Room with ViewModel.

An app that fetches fresh data from a REST API each time it starts can rely on ViewModel alone to cache the response during a session. An app that lets users write notes must write to disk the moment a change is made, with the ViewModel only holding the working copy between writes.

Common pitfalls when retaining data with ViewModel

A frequent mistake is passing the Activity context into a ViewModel. The ViewModel outlives the Activity, so any reference to a View, Context or Fragment inside it is a memory leak waiting to happen. Use the AndroidViewModel subclass only when you genuinely need an Application context.

Another pitfall is treating the ViewModel as a replacement for onSaveInstanceState. ViewModel does not survive process death, so when the system reclaims memory because the user opens another heavy app, you lose the ViewModel too. Always test with the "Don't keep activities" developer option enabled.

Finally, avoid storing large bitmaps or sensitive data such as authentication tokens inside a ViewModel. They belong in encrypted storage, and the ViewModel should hold only a reference to a key or path. The Australian Cyber Security Centre has published guidance on safe storage of credentials, and aligning with that advice is a small step that pays off later.

Practical habits that keep state retention clean

  • Choose between Activity and Fragment scope deliberately, since the ViewModel lives as long as its owner does.
  • Use SavedStateHandle for primitives that must survive process death, leaving the ViewModel focused on in-memory state.
  • Never reference Activity, View or Fragment fields from inside a ViewModel, even through a lambda.
  • Pair ViewModel with LiveData or StateFlow so the UI updates automatically when the underlying data changes.
  • Launch coroutines with viewModelScope and let the framework cancel them on clear.
  • Test retention behaviour with the "Don't keep activities" option turned on in Developer Options.
  • Keep the ViewModel focused; treat it as a controller for one screen, not a global cache.