Everything About Androidβs ViewModel
(Also, a yearly recap for me! π )
Introduction
In modern Android development, managing UI-related data cleanly and efficiently is one of the most critical challenges developers face. Enter ViewModel β a class designed to store and manage UI-related data in a lifecycle-conscious way. Introduced as part of Android's Jetpack Architecture Components, ViewModel has become a cornerstone of robust Android app architecture.
This article covers everything you need to know about ViewModel: what it is, how it works, why it exists, how to use it, and best practices for making the most of it.
1. What Is ViewModel?
ViewModel is a class from the androidx.lifecycle package that is designed to hold and manage UI-related data across configuration changes (like screen rotations). It acts as the data layer between your UI (Activity/Fragment) and your data sources (repository, network, database).
class MyViewModel : ViewModel() {
val userName: MutableLiveData<String> = MutableLiveData()
}
The key property of a ViewModel is its lifecycle awareness: it survives configuration changes, meaning data doesn't need to be re-fetched every time the screen rotates.
2. The Problem ViewModel Solves
Before ViewModel, Android developers faced a classic problem:
- Configuration changes (like rotating the screen) cause the Activity to be destroyed and recreated.
- Any data held in the Activity's member variables is lost.
- Developers resorted to
onSaveInstanceState(), which is limited to small, serializable data. - Network calls were often re-triggered unnecessarily, wasting bandwidth and time.
ViewModel solves this elegantly by decoupling data from the UI lifecycle.
Without ViewModel:
[Activity Created] β [Data Loaded] β [Screen Rotated] β [Activity Destroyed]
β [Activity Created Again] β [Data Loaded Again β]
With ViewModel:
[Activity Created] β [ViewModel Created + Data Loaded]
[Screen Rotated] β [Activity Destroyed & Recreated]
β [Same ViewModel Returned β
] β No re-fetch needed
3. ViewModel Lifecycle
Understanding the ViewModel lifecycle is fundamental. A ViewModel's lifetime is tied to the scope it is created in, not the individual Activity or Fragment.
Activity Start β ViewModel Created
β
Screen Rotation β Activity Destroyed β Activity Re-created β Same ViewModel Returned
β
Activity Finished (Back pressed / finish()) β ViewModel.onCleared() called β ViewModel Destroyed
The onCleared() method is the only lifecycle callback in ViewModel. It's called when the ViewModel is about to be destroyed β useful for cancelling coroutines or releasing resources.
class MyViewModel : ViewModel() {
override fun onCleared() {
super.onCleared()
// Cancel jobs, release resources
}
}
4. Setting Up ViewModel
Dependency
Add the lifecycle dependency to your build.gradle:
// build.gradle.kts
dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.0")
implementation("androidx.activity:activity-ktx:1.9.0") // for by viewModels()
implementation("androidx.fragment:fragment-ktx:1.7.0") // for by viewModels() in Fragments
}
Basic Usage
// ViewModel
class CounterViewModel : ViewModel() {
private val _count = MutableLiveData(0)
val count: LiveData<Int> = _count
fun increment() {
_count.value = (_count.value ?: 0) + 1
}
}
// Activity
class MainActivity : AppCompatActivity() {
private val viewModel: CounterViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel.count.observe(this) { count ->
textView.text = count.toString()
}
button.setOnClickListener {
viewModel.increment()
}
}
}
The by viewModels() delegate handles ViewModel instantiation and scoping automatically.
5. ViewModelProvider and ViewModelStore
Behind the scenes, ViewModels are created and stored using ViewModelProvider and ViewModelStore.
- ViewModelStore: A container that stores ViewModels keyed by a string identifier.
- ViewModelProvider: A factory that creates or retrieves ViewModels from a
ViewModelStore.
// Manual (verbose) approach β rarely needed
val viewModel = ViewModelProvider(this)[CounterViewModel::class.java]
// Preferred Kotlin delegate
val viewModel: CounterViewModel by viewModels()
When an Activity is recreated after a rotation, Android retains the ViewModelStore (as part of the NonConfigurationInstance), which is why the same ViewModel instance is returned.
6. ViewModel with Fragments
Fragment-scoped ViewModel
By default, by viewModels() inside a Fragment scopes the ViewModel to that Fragment:
class MyFragment : Fragment() {
private val viewModel: MyViewModel by viewModels()
}
Activity-scoped ViewModel (Shared Between Fragments)
To share data between Fragments, scope the ViewModel to the parent Activity using activityViewModels():
// FragmentA
class FragmentA : Fragment() {
private val sharedViewModel: SharedViewModel by activityViewModels()
}
// FragmentB
class FragmentB : Fragment() {
private val sharedViewModel: SharedViewModel by activityViewModels()
// Same instance as FragmentA β
}
This is the recommended pattern for Fragment-to-Fragment communication, replacing older callback/interface approaches.
7. ViewModel with Hilt (Dependency Injection)
When using Hilt, you can inject dependencies directly into ViewModels using @HiltViewModel and @Inject:
@HiltViewModel
class UserViewModel @Inject constructor(
private val userRepository: UserRepository
) : ViewModel() {
val users: LiveData<List<User>> = userRepository.getUsers()
}
In your Fragment or Activity:
@AndroidEntryPoint
class UserFragment : Fragment() {
private val viewModel: UserViewModel by viewModels()
// Hilt automatically provides UserRepository β
}
This eliminates the need for custom ViewModelFactory in most cases.
8. ViewModel with Custom Factory
When your ViewModel needs constructor parameters and you're not using Hilt, you need a ViewModelFactory:
class UserViewModel(private val userId: String) : ViewModel() {
// ...
}
class UserViewModelFactory(private val userId: String) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(UserViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return UserViewModel(userId) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
// In Activity/Fragment
val factory = UserViewModelFactory("user_123")
val viewModel: UserViewModel by viewModels { factory }
9. ViewModel + LiveData
LiveData and ViewModel are natural companions. LiveData is a lifecycle-aware observable data holder.
class ProductViewModel : ViewModel() {
private val _products = MutableLiveData<List<Product>>()
val products: LiveData<List<Product>> = _products // Expose as read-only
fun loadProducts() {
// Simulate data loading
_products.value = listOf(Product("Laptop"), Product("Phone"))
}
}
Best practice: Expose LiveData (immutable) to the UI, keep MutableLiveData private inside the ViewModel. This prevents the UI from directly mutating state.
10. ViewModel + Kotlin Coroutines (viewModelScope)
The viewModelScope is a CoroutineScope tied to the ViewModel's lifecycle. Coroutines launched in this scope are automatically cancelled when the ViewModel is cleared.
class NewsViewModel(private val repository: NewsRepository) : ViewModel() {
private val _news = MutableLiveData<List<Article>>()
val news: LiveData<List<Article>> = _news
fun fetchNews() {
viewModelScope.launch {
try {
val articles = repository.getTopHeadlines() // suspend function
_news.value = articles
} catch (e: Exception) {
// Handle error
}
}
}
}
viewModelScope uses Dispatchers.Main.immediate by default, so updating LiveData is safe without explicit withContext(Dispatchers.Main).
11. ViewModel + StateFlow and SharedFlow
In modern Android with Kotlin Flows, StateFlow and SharedFlow are preferred over LiveData for reactive streams.
StateFlow
class LoginViewModel : ViewModel() {
private val _uiState = MutableStateFlow(LoginUiState())
val uiState: StateFlow<LoginUiState> = _uiState.asStateFlow()
fun login(email: String, password: String) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
val result = authRepository.login(email, password)
_uiState.update { it.copy(isLoading = false, isLoggedIn = result.isSuccess) }
}
}
}
data class LoginUiState(
val isLoading: Boolean = false,
val isLoggedIn: Boolean = false,
val errorMessage: String? = null
)
Collecting in the UI
class LoginFragment : Fragment() {
private val viewModel: LoginViewModel by viewModels()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewLifecycleOwner.lifecycleScope.launch {
viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
progressBar.isVisible = state.isLoading
if (state.isLoggedIn) navigateToHome()
}
}
}
}
}
Always use repeatOnLifecycle(Lifecycle.State.STARTED) when collecting flows in the UI to avoid collecting in the background.
12. ViewModel in Jetpack Compose
Compose has first-class support for ViewModel via the hiltViewModel() or viewModel() composable functions:
@Composable
fun CounterScreen(
viewModel: CounterViewModel = viewModel()
) {
val count by viewModel.count.collectAsState()
Column {
Text(text = "Count: $count")
Button(onClick = { viewModel.increment() }) {
Text("Increment")
}
}
}
With Hilt:
@Composable
fun UserScreen(
viewModel: UserViewModel = hiltViewModel()
) {
// viewModel is injected with Hilt β
}
ViewModel is the only way to safely share state across Composable recompositions and configuration changes.
13. SavedStateHandle
SavedStateHandle allows ViewModel to save and restore state across process death (not just configuration changes). It's like onSaveInstanceState but integrated into ViewModel.
class SearchViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// State is persisted across process death
var searchQuery: String
get() = savedStateHandle.get<String>("query") ?: ""
set(value) = savedStateHandle.set("query", value)
// Even better: use StateFlow backed by SavedStateHandle
val queryFlow: StateFlow<String> =
savedStateHandle.getStateFlow("query", "")
}
With Hilt, SavedStateHandle is automatically injected:
@HiltViewModel
class SearchViewModel @Inject constructor(
private val savedStateHandle: SavedStateHandle,
private val searchRepository: SearchRepository
) : ViewModel() { ... }
14. Scoping ViewModels to Navigation Graphs
With the Navigation component, you can scope ViewModels to a navigation graph β shared across all destinations within that graph:
// In a Fragment within the nav graph
val viewModel: CheckoutViewModel by navGraphViewModels(R.id.checkout_graph)
This is useful for multi-step flows (like a checkout process) where multiple screens need to share state without scoping the ViewModel all the way up to the Activity.
15. ViewModel Anti-Patterns to Avoid
Even with a well-designed API, ViewModel is easy to misuse. Here are the most common pitfalls and how to avoid them.
Even with a well-designed API, ViewModel is easy to misuse. Here are the most common pitfalls and how to avoid them.
Holding a reference to Context or View. A ViewModel outlives the Activity or Fragment it serves, so storing a reference to either will prevent garbage collection and cause memory leaks. If you genuinely need a context, use ApplicationContext via AndroidViewModel, or better yet, push that concern into a repository. Never store a View, Activity, or Fragment reference inside a ViewModel.
// β Bad β leaks the Activity's Context
class BadViewModel(private val context: Context) : ViewModel() {
fun getAppName() = context.getString(R.string.app_name)
}
// β
Good β use AndroidViewModel for Application context
class GoodViewModel(application: Application) : AndroidViewModel(application) {
fun getAppName() = getApplication<Application>().getString(R.string.app_name)
}Putting business logic in the Activity or Fragment. When logic lives in the UI layer it becomes tightly coupled to the Android lifecycle and nearly impossible to unit test. Business logic β validation rules, data transformations, use-case orchestration β belongs in the ViewModel or, for heavier concerns, in a dedicated Use Case / Repository layer.
// β Bad β validation logic buried in the Fragment
class LoginFragment : Fragment() {
private fun onLoginClicked() {
val email = emailInput.text.toString()
if (email.isEmpty() || !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
emailInput.error = "Invalid email"
return
}
// proceed...
}
}
// β
Good β logic lives in the ViewModel, easy to unit test
class LoginViewModel : ViewModel() {
fun onLoginClicked(email: String) {
if (email.isEmpty() || !Patterns.EMAIL_ADDRESS.matcher(email).matches()) {
_uiState.update { it.copy(emailError = "Invalid email") }
return
}
// proceed...
}
}Exposing MutableLiveData or MutableStateFlow publicly. Allowing the UI to write directly to mutable state defeats the purpose of having a single source of truth. Always keep the mutable backing property private inside the ViewModel and expose only the immutable LiveData or StateFlow to observers. This enforces a clear, one-directional data flow.
// β Bad β UI can mutate state freely, no controlled flow
class BadViewModel : ViewModel() {
val username = MutableLiveData<String>() // anyone can call .value = "anything"
}
// β
Good β only the ViewModel can write; UI can only observe
class GoodViewModel : ViewModel() {
private val _username = MutableLiveData<String>()
val username: LiveData<String> = _username // read-only for the UI
fun updateUsername(name: String) {
_username.value = name.trim() // controlled write path
}
}Not cancelling coroutines properly. Coroutines started outside of a lifecycle-aware scope can outlive the ViewModel and continue running after the user has left the screen, wasting resources or β worse β updating a UI that no longer exists. Always use viewModelScope for coroutines inside a ViewModel; it is automatically cancelled when onCleared() is called.
// β Bad β coroutine is not tied to the ViewModel's lifecycle
class BadViewModel : ViewModel() {
fun loadData() {
CoroutineScope(Dispatchers.IO).launch { // nothing cancels this
val data = repository.fetchData()
_uiState.value = data // may crash if ViewModel is already cleared
}
}
}
// β
Good β coroutine is automatically cancelled when ViewModel is cleared
class GoodViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
val data = repository.fetchData()
_uiState.value = data
}
}
}Launching coroutines in GlobalScope. This is a specific and common form of the above mistake. GlobalScope coroutines are not tied to any lifecycle and will continue running for the entire lifetime of the process. Treat GlobalScope as off-limits inside a ViewModel and replace every occurrence with viewModelScope.
// β Bad β GlobalScope lives as long as the app process
class BadViewModel : ViewModel() {
fun syncData() {
GlobalScope.launch(Dispatchers.IO) { // outlives the ViewModel
repository.sync()
}
}
}
// β
Good β cancelled automatically when ViewModel is destroyed
class GoodViewModel : ViewModel() {
fun syncData() {
viewModelScope.launch(Dispatchers.IO) {
repository.sync()
}
}
}Treating ViewModel as a catch-all. It can be tempting to dump everything β navigation logic, formatting, analytics, complex domain rules β into the ViewModel. Doing so turns it into a "God object" that is hard to read and test. Keep ViewModels focused on holding and exposing UI state. Delegate domain logic to Use Cases, and let the Repository own all data-access concerns.
// β Bad β ViewModel doing too much
class BadOrderViewModel : ViewModel() {
fun placeOrder(cart: Cart) {
// Formatting
val formattedTotal = "$${"%.2f".format(cart.total)}"
// Network call directly
val response = retrofitService.placeOrder(cart)
// Analytics
FirebaseAnalytics.logEvent("order_placed", cart.toBundle())
// Navigation
_navigateTo.value = OrderConfirmationScreen(response.orderId)
}
}
// β
Good β ViewModel delegates, stays focused on state
class GoodOrderViewModel @Inject constructor(
private val placeOrderUseCase: PlaceOrderUseCase,
private val analyticsTracker: AnalyticsTracker
) : ViewModel() {
fun placeOrder(cart: Cart) {
viewModelScope.launch {
val orderId = placeOrderUseCase(cart) // domain logic in Use Case
analyticsTracker.track("order_placed") // analytics abstracted away
_uiState.update { it.copy(confirmedOrderId = orderId) }
}
}
}16. Unit Testing ViewModels
ViewModels are designed to be easily testable since they don't hold Android framework references.
@OptIn(ExperimentalCoroutinesApi::class)
class CounterViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private lateinit var viewModel: CounterViewModel
@Before
fun setup() {
viewModel = CounterViewModel()
}
@Test
fun `increment increases count by 1`() = runTest {
viewModel.increment()
assertEquals(1, viewModel.count.value)
}
}
For coroutine testing, use kotlinx-coroutines-test with a TestCoroutineDispatcher or the MainDispatcherRule.
17. MVVM Architecture with ViewModel
ViewModel is the "VM" in MVVM (Model-View-ViewModel) pattern, which is the officially recommended architecture for Android:
βββββββββββββββββββββββββββββββββββββββββββββββ
β View β
β (Activity / Fragment / Compose) β
β Observes LiveData/StateFlow, sends events β
βββββββββββββββββββ¬ββββββββββββββββββββββββββββ
β observes / calls
βββββββββββββββββββΌββββββββββββββββββββββββββββ
β ViewModel β
β Holds UI state, business logic, use cases β
βββββββββββββββββββ¬ββββββββββββββββββββββββββββ
β calls
βββββββββββββββββββΌββββββββββββββββββββββββββββ
β Repository β
β Abstracts data sources (DB, Network) β
ββββββββββββ¬βββββββββββββββββββββββ¬ββββββββββββ
β β
ββββββββΌβββββββ ββββββββΌβββββββ
β Local DB β β Remote API β
β (Room) β β (Retrofit) β
βββββββββββββββ βββββββββββββββ
18. ViewModel vs Other State Management
Choosing the right state management mechanism depends on two key questions: does the state need to survive a configuration change (screen rotation), and does it need to survive process death (the OS killing the app in the background)? Here is how each option stacks up.
ViewModel survives configuration changes, which is its primary strength. However, it does not survive process death on its own β pairing it with SavedStateHandle is required for full persistence. Its scope can be tied to an Activity, a Fragment, or an entire Navigation Graph, making it the most flexible option for screen-level state.
onSaveInstanceState survives both configuration changes and process death, but it is limited to small amounts of serializable data and is scoped only to Activities and Fragments. It works well as a complement to ViewModel via SavedStateHandle, but is not practical as a standalone state store for complex UI state.
rememberSaveable (Compose) is the Compose equivalent of onSaveInstanceState. It survives both configuration changes and process death, and its scope is limited to the Composable it is declared in. It is ideal for transient local UI state β such as scroll position or a text field value β within a single Composable.
remember (Compose) is the most lightweight option. It only keeps state alive across recompositions within the same composition and is lost on both configuration changes and process death. Use it purely for ephemeral, in-memory state that has no need to outlive the current screen instance.
Application class fields survive configuration changes because the Application object lives for the entire process lifetime. However, they are wiped on process death, have no built-in lifecycle awareness, and are globally accessible β making them prone to misuse. Prefer a properly scoped ViewModel or a singleton repository over storing state directly on the Application.
19. Summary of Key APIs
| API | Purpose |
|---|---|
ViewModel |
Base class for all ViewModels |
AndroidViewModel |
ViewModel with Application context |
ViewModelProvider |
Creates/retrieves ViewModel instances |
by viewModels() |
Kotlin delegate for Activity/Fragment |
by activityViewModels() |
Scope ViewModel to Activity from Fragment |
by navGraphViewModels() |
Scope ViewModel to Navigation Graph |
viewModelScope |
CoroutineScope tied to ViewModel lifecycle |
SavedStateHandle |
Persist state across process death |
onCleared() |
Called when ViewModel is about to be destroyed |
Conclusion
ViewModel is one of the most impactful components in the Android Jetpack ecosystem. By providing a clean separation between UI and data, surviving configuration changes, integrating seamlessly with LiveData, Coroutines, StateFlow, Hilt, and Jetpack Compose, it enables developers to build apps that are robust, testable, and maintainable.
Whether you're building a simple counter app or a complex multi-screen application, ViewModel should be a fundamental part of your architecture. Start with simple ViewModels, adopt viewModelScope for async work, use SavedStateHandle for process-death safety, and gradually evolve toward a full MVVM architecture as your app grows.
Written for Android developers using Kotlin and Jetpack β targeting API 21+ with Lifecycle 2.8+