ViewModel: How Configuration Change Survival Actually Works
ViewModel: How Configuration Change Survival Actually Works
Android's ViewModel is one of the most widely used architecture components, yet its core survival mechanism remains a mystery to most developers. You annotate a class, call viewModels() in your Activity, and your state magically survives screen rotation. But what actually happens behind the scenes? The answer involves a retained in memory object that is never serialized, a simple HashMap keyed by strings, a carefully ordered resource cleanup sequence, and a factory system that separates creation from retrieval.
In this article, you'll dive deep into the internal machinery that makes ViewModel survive configuration changes, exploring how ComponentActivity retains the ViewModelStore through Android's NonConfigurationInstances mechanism, how ViewModelProvider coordinates thread safe retrieval and creation through ViewModelProviderImpl, how ViewModelImpl manages resource lifecycle with a deliberate clearing order, how CreationExtras enables stateless factory injection, and how fragments piggyback on this entire system through FragmentManagerViewModel. This isn't a guide on using ViewModel. It's an exploration of the retention, creation, and destruction machinery that makes configuration change survival possible.
The fundamental problem: State that outlives Activity instances
Consider this common scenario:
class CounterActivity : ComponentActivity() {
private var count = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
Button(onClick = { count++ }) {
Text("Count: $count")
}
}
}
}
Rotate the device, and count resets to zero. The Android framework destroys and recreates the Activity on configuration changes. Every field, every local variable, every reference is gone.
The Bundle approach works for small, serializable data:
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putInt("count", count)
}
But Bundles have a strict 1MB transaction limit, can only hold primitive and parcelable types, and require manual serialization and deserialization. What about a list of 10,000 items fetched from a network request? A database cursor? A WebSocket connection? These cannot be serialized into a Bundle.
ViewModel solves this by retaining the object in memory across configuration changes. Not serialized. Not parceled. The exact same object instance, held in memory while the old Activity is destroyed and the new one is created.
ViewModelStore: The retention container
At the foundation of the system is ViewModelStore, a wrapper around a MutableMap:
public open class ViewModelStore {
private val map = mutableMapOf<String, ViewModel>()
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public fun put(key: String, viewModel: ViewModel) {
val oldViewModel = map.put(key, viewModel)
oldViewModel?.clear()
}
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
public operator fun get(key: String): ViewModel? = map[key]
public fun clear() {
for (vm in map.values) {
vm.clear()
}
map.clear()
}
}
Three design decisions stand out:
String keys, not type keys. ViewModels are stored by string, not by class. This allows multiple instances of the same ViewModel class to coexist in a single store, each with a different key. The default key is generated from the class name, but custom keys enable advanced patterns.
Replace with immediate cleanup. When put() is called with an existing key, the old ViewModel is immediately cleared via oldViewModel?.clear(). This prevents resource leaks when a ViewModel is replaced, which can happen if a factory produces a different instance for the same key.
Clear iterates all values. The clear() method calls vm.clear() on every stored ViewModel before clearing the map. This guarantees that all resources (coroutine scopes, database connections, streams) are released when the store is permanently destroyed.
The ViewModelStore itself has no awareness of configuration changes, lifecycle events, or the Android framework. It's a container with cleanup semantics. The retention logic lives elsewhere.
NonConfigurationInstances: The retention mechanism
The actual survival mechanism lives in ComponentActivity. When a configuration change occurs, the Android framework calls onRetainNonConfigurationInstance() before destroying the Activity. Whatever object this method returns stays in memory and is available to the new Activity instance via lastNonConfigurationInstance.
ComponentActivity uses this to retain the ViewModelStore:
internal class NonConfigurationInstances {
var custom: Any? = null
var viewModelStore: ViewModelStore? = null
}
The NonConfigurationInstances class holds two things: the ViewModelStore and a deprecated custom field for backward compatibility. When a configuration change is triggered, onRetainNonConfigurationInstance() packages the current ViewModelStore into this container:
final override fun onRetainNonConfigurationInstance(): Any? {
val custom = onRetainCustomNonConfigurationInstance()
var viewModelStore = _viewModelStore
if (viewModelStore == null) {
val nc = lastNonConfigurationInstance as NonConfigurationInstances?
if (nc != null) {
viewModelStore = nc.viewModelStore
}
}
if (viewModelStore == null && custom == null) {
return null
}
val nci = NonConfigurationInstances()
nci.custom = custom
nci.viewModelStore = viewModelStore
return nci
}
This article continues for subscribers
Subscribe to Dove Letter for full access to 40+ deep-dive articles about Android and Kotlin development.
Become a Sponsor