Picture this: you are building a simple to-do list app. The user adds a task, rotates the phone, and the task disappears. The app crashed? No—Android just recreated the activity, and you lost the state. That is the problem ViewModel was built to solve. But as we will see, ViewModel is not just a survival kit for configuration changes. It is a state container that lives as long as the screen's lifecycle, and if used carelessly, it can also hold on to data longer than you want. This guide is for developers who have built a few Android screens—maybe in Jetpack Compose or traditional XML—and want to understand state management beyond copying code from a tutorial. We will cover what ViewModel does, how to use it correctly, common pitfalls, and when you might be better off without it.
Why State Management Matters in Everyday Android Work
Think of a screen as a temporary workspace. When the user rotates the phone, Android tears down the activity and rebuilds it. Any variable you stored in the activity is gone. That is fine for a loading flag that you can re-fetch, but not for a multi-step form or a list of items the user just edited. ViewModel acts like a desk drawer: the workspace gets rebuilt, but the drawer stays. The ViewModel survives configuration changes because it is scoped to the lifecycle of the host—usually a navigation destination or an activity.
In a typical project, you might have a screen that loads data from a remote API, lets the user filter results, and then navigates to a detail screen. Without ViewModel, you would have to save and restore state manually in onSaveInstanceState—and that only works for primitive types or serializable objects. With ViewModel, you keep your data in a LiveData or StateFlow, and the UI observes it. When the activity is recreated, the ViewModel is still there, and the UI reconnects automatically.
We often see teams start with ViewModel for every screen, even for simple read-only pages. That is not wrong, but it adds boilerplate. The key is to recognize where state management truly helps: screens with user input, dynamic lists, or data that takes more than a few milliseconds to load. For a static about-us screen, a ViewModel might be overkill.
How ViewModel Differs from Saving State in Bundles
Android's onSaveInstanceState is designed for small amounts of UI state—like a scroll position or a text field value—that can be serialized. It is not meant for complex objects or large lists. ViewModel, on the other hand, can hold any object in memory. The trade-off is that ViewModel lives until the host finishes (e.g., the activity is finished, not just rotated), so you need to be careful not to keep references to things that should be garbage-collected.
What ViewModel Actually Does Under the Hood
When you call ViewModelProvider.get(MyViewModel::class.java), Android checks if a ViewModel already exists for that scope. If it does, it returns the existing instance. If not, it creates a new one and stores it in a map that is retained across configuration changes. The ViewModel is not destroyed until the scope's onCleared() is called, which happens when the activity is finishing or the fragment is detached permanently.
This mechanism is simple but powerful. It means your ViewModel can hold any data—lists, objects, even network responses—without worrying about the screen being recreated. But there is a catch: the ViewModel does not know about the UI lifecycle. If you hold a reference to a Context or a View, you can leak memory because the ViewModel outlives the activity. That is why you should never pass an activity reference into a ViewModel constructor. Instead, use AndroidViewModel if you need the application context, or better, use a repository pattern.
The Lifecycle of a ViewModel
A ViewModel is created when you first request it for a given scope (activity or fragment). It is cleared when the scope is permanently destroyed. For an activity, that means onDestroy() is called without a subsequent onCreate()—i.e., the activity is finishing, not just rotating. For a fragment, it is when the fragment is detached and its host activity is finishing. Understanding this timing is crucial to avoid doing heavy work in onCleared() that might block the main thread.
Patterns That Usually Work: LiveData, StateFlow, and Coroutines
The most common pattern is to expose a LiveData or StateFlow from the ViewModel, and have the UI observe it. For example, a ViewModel might fetch a list of tasks from a repository and expose it as StateFlow<List<Task>>. The Compose screen collects this flow and renders the list. When the user adds a task, the ViewModel calls a repository method and updates the state.
Another pattern that works well is using viewModelScope for coroutines. This scope is automatically cancelled when the ViewModel is cleared, so you do not have to manage coroutine jobs manually. For example, you can launch a network request inside viewModelScope.launch and the coroutine will be cancelled if the user leaves the screen permanently.
Choosing Between LiveData and StateFlow
LiveData is lifecycle-aware and works well with XML-based UI. StateFlow is preferred in Compose because it integrates with Kotlin flows and is not tied to Android framework classes. Both are fine; the important thing is to pick one and be consistent. We lean toward StateFlow for new projects because it is more flexible and works with coroutines seamlessly.
Handling One-Time Events
One common mistake is to expose a one-time event (like a navigation action) as a state. If you expose it as a LiveData or StateFlow, the UI might re-observe the old event after a configuration change, causing navigation to happen again. The typical fix is to use a SingleLiveEvent or a Channel with replay = 0. In Compose, you can use a SharedFlow with replay = 0 and collect it in a LaunchedEffect.
Anti-Patterns That Lead to Bugs and Memory Leaks
The most common anti-pattern is storing a reference to the activity or a view inside the ViewModel. This can happen when you pass a Context directly or when you use a listener that holds a reference to the UI. The ViewModel outlives the activity, so the activity cannot be garbage-collected, causing a memory leak. Always use the application context if you need a context, and never pass views or adapters.
Another anti-pattern is putting too much logic in the ViewModel. We have seen ViewModels that handle network calls, database operations, and UI formatting all in one class. This makes testing difficult and violates the single responsibility principle. Instead, move business logic to repositories or use cases, and keep the ViewModel as a coordinator that transforms data for the UI.
Over-Observing and Retriggering Work
If you start a network request in the ViewModel's init block, it will run once per ViewModel instance. That is usually fine. But if you start it in a function that the UI calls every time it observes, you might retrigger the request on every configuration change. The fix is to use a flag or a while loop with a condition, or better, use a pattern like StateFlow with a single emission.
Holding Large Data Sets Indefinitely
If your ViewModel holds a large list of items (say, thousands of database records), it stays in memory as long as the ViewModel lives. If the user navigates away and comes back, the ViewModel might still be alive (if the host is not finished), and you are holding that large list unnecessarily. Consider using paging or clearing the data when the screen is not visible.
Maintenance Costs and Long-Term Drift
Over time, ViewModels tend to accumulate state. A screen that started with a simple boolean might end up with a dozen state variables, all exposed as separate LiveData or StateFlow objects. This makes the ViewModel hard to read and test. The solution is to use a single state object (a data class) that holds all the UI state. For example, instead of having isLoading, error, and tasks as separate flows, combine them into a TaskScreenState data class. This reduces the number of observers and makes the state atomic.
Another long-term cost is that ViewModels can become a dumping ground for any logic that does not fit elsewhere. We have seen ViewModels that contain validation, formatting, and even animation logic. This makes the ViewModel hard to test and reuse. A good rule of thumb: if the logic does not involve coordinating between the UI and the data layer, it probably belongs elsewhere.
Testing ViewModels
Testing a ViewModel is straightforward because it does not depend on Android framework components (except for AndroidViewModel). You can create an instance, call methods, and assert on the state. The challenge is testing coroutines: you need to use runBlockingTest or runTest from kotlinx-coroutines-test. Also, if your ViewModel uses viewModelScope, you can mock it or use a test dispatcher.
When Not to Use ViewModel
ViewModel is not the only tool for state management. For simple screens that do not survive configuration changes (e.g., a dialog that you want to dismiss on rotation), you might be better off using remember in Compose or onSaveInstanceState. For global state that should persist across screens (like a user session), consider using a singleton or a dependency injection container instead of a ViewModel that is scoped to a single screen.
Another scenario is when you need to share state between two screens that are not parent-child. ViewModel is scoped to an activity or a navigation graph, so if you need to share data between two fragments that are not in the same activity, you might need a shared ViewModel or a repository. But be careful: a shared ViewModel can become a mess if too many screens depend on it.
Alternatives to ViewModel
For Compose-only apps, you can use remember and mutableStateOf for local UI state. For more complex state, you can use a state holder class that is not a ViewModel but is created with remember. This avoids the lifecycle overhead of ViewModel. However, you lose the automatic survival across configuration changes. If your screen does not need to survive rotation, this is fine.
Open Questions and Common Misunderstandings
One question we often hear is: should I use a ViewModel for every fragment? The answer is no. If the fragment only displays static data that is passed via arguments, you do not need a ViewModel. Another question: can I use ViewModel with a service? No, ViewModel is designed for UI components. For background work, use a Worker or a Service.
Another misunderstanding is that ViewModel automatically survives process death. It does not. If the app is killed by the system, the ViewModel is destroyed. To survive process death, you need to use SavedStateHandle in the ViewModel, which saves and restores state via the saved instance state bundle. This is a good practice for important data like form inputs.
FAQ
Q: Can I inject dependencies into ViewModel? Yes, using a factory or a dependency injection framework like Hilt or Koin. Hilt provides @HiltViewModel which automatically creates a factory.
Q: Should I use one ViewModel per screen or one per feature? Usually one per screen (activity or fragment). For a multi-step flow, you might use a shared ViewModel scoped to the navigation graph.
Q: Is ViewModel thread-safe? Not inherently. You should update state from the main thread, or use thread-safe collections. StateFlow and LiveData are thread-safe for observation, but the underlying data must be accessed from a single thread (usually the main thread).
Summary and Next Experiments
ViewModel is a practical tool for managing UI state across configuration changes, but it is not a silver bullet. Start by using it for screens that have user input or dynamic data. Keep your ViewModel lean—move business logic to repositories, use a single state object, and avoid holding references to the UI. Test your ViewModel in isolation using coroutine test dispatchers.
For your next project, try these experiments: (1) Convert an existing screen that uses onSaveInstanceState to use ViewModel and see how much code you remove. (2) Introduce a single state data class and measure how much easier it is to reason about the screen. (3) Try using SavedStateHandle to survive process death and compare it to restoring state manually. These small changes will deepen your understanding and help you decide when ViewModel is the right choice.
Comments (0)
Please sign in to post a comment.
Don't have an account? Create one
No comments yet. Be the first to comment!