Stopping the Rotation Crash: Managing UI State with ViewModel and LiveData
Learn how to use ViewModel and LiveData to prevent data loss and crashes during Android configuration changes like screen rotations.
22 Jul 2025, 22:01 UTC

The Configuration Change Headache
Android developers often face a frustrating cycle: a user enters data into a form or triggers a network request, rotates their device, and suddenly the app crashes or the screen wipes clean. This happens because Android destroys and recreates the Activity during a configuration change (like screen rotation or language switching) to reload the appropriate resources.
While onSaveInstanceState() handles small amounts of data, it is unsuitable for large lists or active network streams. The solution is to move UI state out of the Activity lifecycle and into a ViewModel, using LiveData to pipe that data back to the UI safely.
Decoupling State from the Lifecycle
A ViewModel is designed to outlive the specific instance of an Activity or Fragment. When the Activity is destroyed during rotation, the ViewModel remains in memory. Once the new Activity instance is created, it reconnects to the existing ViewModel, allowing the UI to restore its state instantly without re-fetching data from a database or API.
However, the ViewModel must remain agnostic of the UI's specific instance. Never store a reference to an Activity, Fragment, or View inside a ViewModel. Doing so creates a memory leak because the ViewModel will hold onto the destroyed Activity, preventing the Garbage Collector from reclaiming that memory.
Lifecycle-Aware Updates with LiveData
If a ViewModel simply held a variable, the Activity would have to manually poll for changes. LiveData solves this by acting as an observable data holder. It is "lifecycle-aware," meaning it knows the state of the Activity observing it.
- Active State: LiveData only notifies the observer if the Activity is in the
STARTEDorRESUMEDstate. - Automatic Cleanup: When the Activity is finally destroyed (not just rotated), the observer is automatically removed, preventing the common "IllegalStateException" that occurs when trying to update a UI element that no longer exists.
Implementation Example: A Simple Counter
In this example, we maintain a counter that persists across rotations. This requires the androidx.lifecycle:lifecycle-viewmodel and androidx.lifecycle:lifecycle-livedata dependencies.
The ViewModel
// Run this in your app module
class CounterViewModel : ViewModel() {
// Private MutableLiveData prevents the UI from modifying the data directly
private val _count = MutableLiveData<Int>(0)
// Public immutable LiveData for the Activity to observe
val count: LiveData<Int> = _count
fun increment() {
val current = _count.value ?: 0
_count.value = current + 1
}
}
The Activity
class CounterActivity : AppCompatActivity() {
private lateinit var viewModel: CounterViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Initialize ViewModel via ViewModelProvider to ensure it survives rotation
viewModel = ViewModelProvider(this).get(CounterViewModel::class.java)
val textView = findViewById<TextView>(R.id.countText)
val button = findViewById<Button>(R.id.incrementBtn)
// Observe the LiveData. 'this' refers to the LifecycleOwner
viewModel.count.observe(this) { newCount ->
textView.text = "Count: $newCount"
}
button.setOnClickListener { viewModel.increment() }
}
}
Trade-offs and Limitations
While this pattern solves state persistence, it introduces a risk of "ViewModel Bloat." Developers often mistakenly put business logic, database queries, and network calls directly into the ViewModel. This makes the class difficult to test and maintain.
To avoid this, use the ViewModel only as a coordinator. Delegate data fetching to a Repository class and complex logic to UseCases. The ViewModel should only hold the state necessary for the UI and call the appropriate domain layer methods.
Verifying the Implementation
To ensure your implementation is working correctly, perform these three checks:
- Rotation Test: Increment the counter and rotate the device. The number should remain unchanged and not reset to zero.
- Background Test: Trigger a data update (e.g., via a timer) and move the app to the background. The UI should not attempt to update until the app returns to the foreground.
- Memory Leak Check: Use the Android Studio Profiler. Finish the Activity and trigger a Garbage Collection (GC) event. Ensure the
CounterActivityinstance is removed from the heap and not held by theCounterViewModel.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.