How ViewModel Survives Rotation
A ViewModel is NOT recreated when the Activity is destroyed due to a configuration change (rotation, locale change, etc.). Understanding why requires knowing the ViewModelStore.
The ViewModelStore Chain
Activity (destroyed on rotation)
└── ViewModelStore (SURVIVES rotation — held by NonConfigurationInstance)
└── HashMap<String, ViewModel>
└── "UserViewModel" → UserViewModel instance
When the system destroys an Activity for configuration change, it calls Activity.onRetainNonConfigurationInstance() and stores the ViewModelStore. When the new Activity instance is created, it retrieves the store via getLastNonConfigurationInstance(). The ViewModels inside are never destroyed.
// What the framework does internally (simplified)
class ComponentActivity : Activity(), ViewModelStoreOwner {
private var viewModelStore: ViewModelStore? = null
override fun getViewModelStore(): ViewModelStore {
if (viewModelStore == null) {
// Try to restore from NonConfigurationInstance first
val nc = lastNonConfigurationInstance as? NonConfigurationInstances
viewModelStore = nc?.viewModelStore ?: ViewModelStore()
}
return viewModelStore!!
}
override fun onRetainNonConfigurationInstance(): Any {
return NonConfigurationInstances(viewModelStore = viewModelStore)
}
override fun onDestroy() {
super.onDestroy()
// Only clear on genuine finish, not on rotation
if (!isChangingConfigurations) {
viewModelStore?.clear() // calls ViewModel.onCleared()
}
}
}
Process Death vs Configuration Change
| Scenario | ViewModel survives? | SavedStateHandle survives? |
|---|---|---|
| Rotation | YES | YES |
| Back pressed | NO | NO |
| Process killed by system | NO | YES (via saved state) |
| Force stop / user kills app | NO | NO |
This is the critical distinction: ViewModel is not a persistence mechanism. For true persistence across process death, use SavedStateHandle.
The Factory Pattern
ViewModels cannot be constructed directly by the Activity — they need to go through ViewModelProvider so the framework can manage the store. The factory controls construction.
// Without Hilt — manual factory
class UserViewModel(
private val userId: String,
private val getUserUseCase: GetUserUseCase
) : ViewModel() {
companion object {
fun factory(userId: String, useCase: GetUserUseCase) =
object : ViewModelProvider.Factory {
@Suppress("UNCHECKED_CAST")
override fun <T : ViewModel> create(modelClass: Class<T>): T {
return UserViewModel(userId, useCase) as T
}
}
}
}
// Usage in Fragment
class UserFragment : Fragment() {
private val userId by lazy { requireArguments().getString("userId")!! }
private val viewModel: UserViewModel by viewModels {
UserViewModel.factory(userId, GetUserUseCase(UserRepository()))
}
}
Hilt ViewModel
Hilt automates factory creation. @HiltViewModel + @Inject is all that's needed.
@HiltViewModel
class UserViewModel @Inject constructor(
private val getUserUseCase: GetUserUseCase,
private val savedStateHandle: SavedStateHandle // injected automatically
) : ViewModel() {
// Read navigation argument from SavedStateHandle
private val userId: String = checkNotNull(savedStateHandle["userId"]) {
"userId argument is required"
}
}
// Fragment — no factory, no arguments bundle
@AndroidEntryPoint
class UserFragment : Fragment() {
private val viewModel: UserViewModel by viewModels()
// Hilt wires up the factory; SavedStateHandle is populated from
// the Fragment's arguments Bundle automatically
}
SavedStateHandle Deep Dive
SavedStateHandle is backed by the Activity's saved instance state (Bundle). It survives process death because the system saves the Bundle to disk and restores it on relaunch.
@HiltViewModel
class SearchViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val searchRepository: SearchRepository
) : ViewModel() {
// Persisted across process death AND rotation
// Key "query" maps to Bundle key
private val queryFlow: StateFlow<String> =
savedStateHandle.getStateFlow("query", initialValue = "")
// Updating SavedStateHandle writes to the Bundle
fun onQueryChanged(query: String) {
savedStateHandle["query"] = query
// queryFlow automatically emits the new value
}
// Custom objects require a Parcelable or you must use primitive types
// SavedStateHandle limits: same ~1MB limit as Activity saved state
}
Bundle Size Limit
SavedStateHandle is backed by a Bundle — the same ~1 MB limit applies. Storing large lists causes TransactionTooLargeException. Correct approach:
// Wrong: storing large data in SavedStateHandle
savedStateHandle["allUsers"] = userList // potentially huge!
// Correct: store only the key, reload data from repository
savedStateHandle["selectedUserId"] = userId // just the identifier
// Then reload the user from Room/network using that ID
getStateFlow()
getStateFlow() creates a StateFlow backed by SavedStateHandle — the flow survives process death:
@HiltViewModel
class FilterViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val sortOrder: StateFlow<SortOrder> =
savedStateHandle.getStateFlow("sort_order", SortOrder.NEWEST)
val filterCategory: StateFlow<String> =
savedStateHandle.getStateFlow("filter_category", "all")
fun setSortOrder(order: SortOrder) {
savedStateHandle["sort_order"] = order.name
}
}
ViewModelStoreOwner Hierarchy
Different components provide different ViewModel scopes:
// Activity-scoped: survives Fragment transactions within same Activity
class SharedViewModel : ViewModel() { ... }
class OrderSummaryFragment : Fragment() {
// activityViewModels() uses the Activity's ViewModelStore
private val sharedVm: SharedViewModel by activityViewModels()
}
class PaymentFragment : Fragment() {
// Same instance as OrderSummaryFragment's sharedVm
private val sharedVm: SharedViewModel by activityViewModels()
}
// NavGraph-scoped: survives within a navigation sub-graph
class CheckoutViewModel : ViewModel() { ... }
class ShippingFragment : Fragment() {
// Scoped to the checkout nav graph, shared with all fragments in it
private val checkoutVm: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)
}
Common Mistake: Passing Context to ViewModel
ViewModels are not tied to the Activity lifecycle for a reason. Storing a reference to an Activity or Fragment causes memory leaks.
// WRONG — ViewModel holds Activity reference
class BadViewModel(private val context: Context) : ViewModel() {
// If context is an Activity, you've leaked it!
}
// WRONG — even worse
class WorsViewModel(private val activity: MainActivity) : ViewModel()
// CORRECT — use AndroidViewModel for application context
class GoodViewModel(application: Application) : AndroidViewModel(application) {
// getApplication() returns Application context, not Activity
private val appContext: Context = getApplication()
}
// BETTER — inject application context via Hilt
@HiltViewModel
class BetterViewModel @Inject constructor(
@ApplicationContext private val context: Context
) : ViewModel()
// BEST — don't pass Context at all; pass only what you need
@HiltViewModel
class BestViewModel @Inject constructor(
private val resourceProvider: ResourceProvider // abstraction over Context
) : ViewModel()
ViewModel and onCleared()
onCleared() is called when the ViewModel is permanently destroyed (user navigates back, Activity genuinely finishes). Use it for cleanup, but with viewModelScope coroutines are cancelled automatically.
@HiltViewModel
class StreamViewModel @Inject constructor(
private val webSocketClient: WebSocketClient
) : ViewModel() {
private val job = viewModelScope.launch {
webSocketClient.messages().collect { message ->
// handle messages
}
}
// job is cancelled automatically by viewModelScope when onCleared() is called
override fun onCleared() {
super.onCleared()
// Only needed for non-coroutine resources
webSocketClient.close()
}
}
Key Takeaways
| Concept | Summary |
|---|---|
| ViewModelStore | Held by NonConfigurationInstance; survives rotation |
| Process death | ViewModel does NOT survive; SavedStateHandle does |
| Factory pattern | Required to inject parameters into ViewModel |
@HiltViewModel | Auto-generates factory; supports SavedStateHandle injection |
SavedStateHandle | Backed by Bundle; 1 MB limit; store IDs not large data |
getStateFlow() | Converts SavedStateHandle key to StateFlow |
| Context leak | Never store Activity/Fragment reference in ViewModel |
onCleared() | viewModelScope cancels coroutines automatically |