The ViewModel
class is a business logic or screen level state holder.
It exposes state to the UI and encapsulates related business logic. Its principal advantage is that it caches state and persists it through configuration changes.
This means that your UI doesn’t have to fetch data again when navigating between activities, or following configuration changes, such as when rotating the screen.
ViewModel benefits
The alternative to a ViewModel is a plain class that holds the data you display in your UI. This can become a problem when navigating between activities or Navigation destinations. Doing so destroys that data if you don't store it using the saving instance state mechanism. ViewModel provides a convenient API for data persistence that resolves this issue.
A plain class doesn't know the lifecycle. When you instantiate a class A in the MainActivity and transition to the UserActivity, the data stored within class A will be lost. This also happens during configuration changes and alterations in the UI state prompted by the lifecycle.
Persistence
Even though the vast majority of business logic is present in the data layer, the UI layer can also contain business logic. This can be the case when combining data from multiple repositories to create the screen UI state, or when a particular type of data doesn't require a data layer.
ViewModel is the right place to handle business logic in the UI layer. The ViewModel is also in charge of handling events and delegating them to other layers of the hierarchy when business logic needs to be applied to modify application data.
Scope
When you instantiate a ViewModel, you pass it an object that implements the ViewModelStoreOwner
interface. This may be a Navigation destination, Navigation graph, activity, fragment, or any other type that implements the interface. Your ViewModel is then scoped to the Lifecycle of the ViewModelStoreOwner
. It remains in memory until its ViewModelStoreOwner
goes away permanently.
A range of classes are either direct or indirect subclasses of the ViewModelStoreOwner
interface. The direct subclasses are ComponentActivity
, Fragment
, and NavBackStackEntry
. For a full list of indirect subclasses, see the ViewModelStoreOwner
reference.
class B { }
class C extends B { } // C is B's direct subclass
class A extends C { } // A is B's indirect subclass
When the fragment or activity to which the ViewModel is scoped is destroyed, asynchronous work continues in the ViewModel that is scoped to it. This is the key to persistence.
SavedStateHandle
SaveStateHandle allows you to persist data not just through configuration changes, but across process recreation. That is, it enables you to keep the UI state intact even when the user closes the app and opens it at a later time.
Access to business logic
Even though the vast majority of business logic is present in the data layer, the UI layer can also contain business logic. This can be the case when combining data from multiple repositories to create the screen UI state, or when a particular type of data doesn't require a data layer.
ViewModel is the right place to handle business logic in the UI layer. The ViewModel is also in charge of handling events and delegating them to other layers of the hierarchy when business logic needs to be applied to modify application data.
Jetpack Compose
When using Jetpack Compose, ViewModel is the primary means of exposing screen UI state to your composables. In a hybrid app, activities and fragments simply host your composable functions. This is a shift from past approaches, where it wasn't that simple and intuitive to create reusable pieces of UI with activities and fragments, which caused them to be much more active as UI controllers.
The most important thing to keep in mind when using ViewModel with Compose is that you cannot scope a ViewModel to a composable. This is because a composable is not a ViewModelStoreOwner
. Two instances of the same composable in the Composition, or two different composables accessing the same ViewModel type under the same ViewModelStoreOwner
would receive the same instance of the ViewModel, which often is not the expected behavior.
To get the benefits of ViewModel in Compose, host each screen in a Fragment or Activity, or use Compose Navigation and use ViewModels in composable functions as close as possible to the Navigation destination. That is because you can scope a ViewModel to Navigation destinations, Navigation graphs, Activities, and Fragments.
Implement a View Model
The following is an example implementation of a ViewModel for a screen that allows the user to roll dice.
data class DiceUiState(
val firstDieValue: Int? = null,
val secondDieValue: Int? = null,
val numberOfRolls: Int = 0,
)
class DiceRollViewModel : ViewModel() {
// Expose screen UI state
private val _uiState = MutableStateFlow(DiceUiState())
// Read _uiState as StateFlow type
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Handle business logic
fun rollDice() {
// State update
_uiState.update { currentState ->
currentState.copy(
firstDieValue = Random.nextInt(from = 1, until = 7),
secondDieValue = Random.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
}
You can then access the ViewModel from an activity as follows:
import androidx.activity.viewModels
class DiceRollActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
// Create a ViewModel the first time the system calls an activity's onCreate() method.
// Re-created activities receive the same DiceRollViewModel instance created by the first activity.
// Use the 'by viewModels()' Kotlin property delegate
// from the activity-ktx artifact
val viewModel: DiceRollViewModel by viewModels()
lifecycleScope.launch { //
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect {
// Update UI elements
}
}
}
}
}
lifecycleScope
provides a coroutine scope associated with the lifecycle. It automatically manages coroutines based on specific states of the lifecycle.The
launch
function initiates a new coroutine for performing asynchronous tasks in the background. It is utilized for executing asynchronous operations without blocking the UI thread.The
repeatOnLifecycle
function is useful when used in conjunction withlifecycleScope.launch
to ensure execution only in specific lifecycle states. It allows control over coroutines based on the lifecycle states of an activity or fragment.The
state
parameter of therepeatOnLifecycle
function represents one of the various states of the lifecycle.Lifecycle.State.CREATE
- This state indicates that an activity or fragment has been created but is not yet active. It enters this state after the execution of the
onCreate
method.
- This state indicates that an activity or fragment has been created but is not yet active. It enters this state after the execution of the
Lifecycle.State.STARTED
- In this state, the activity is visible on the screen, and a fragment is in a state where it is presented to the user. It transitions to this state after the execution of the
onStart
method.
- In this state, the activity is visible on the screen, and a fragment is in a state where it is presented to the user. It transitions to this state after the execution of the
Lifecycle.State.RESUME
- The activity is in the foreground and is interactable with the user. It enters this state after the execution of the
onResume
method.
- The activity is in the foreground and is interactable with the user. It enters this state after the execution of the
Lifecycle.State.DESTORYED
- This state signifies that an activity or fragment has been destroyed and is no longer accessible. It enters this state after the execution of the
onDestroy
method.
- This state signifies that an activity or fragment has been destroyed and is no longer accessible. It enters this state after the execution of the
User coroutines with ViewModel
ViewModel
includes support for Kotlin coroutines. It is able to persist asynchronous work in the same manner as it persists UI state.
Clearing ViewModel dependencies
The lifecycle of a ViewModel
is tied directly to its scope. A ViewModel
remains in memory until the ViewModelStoreOwner
to which it is scoped disappears. This may occur in the following contexts:
Here's are the cases, when
ViewModelStoreOwner
disappears.
In the case of an activity, when it finishes.
In the case of a fragment, when it detaches.
In the case of a Navigation entry, when it's removed from the back stack.
This makes ViewModels a great solution for storing data that survives configuration changes.
Figure 1 illustrates the various lifecycle states of an activity as it undergoes a rotation and then is finished. The illustration also shows the lifetime of the ViewModel
next to the associated activity lifecycle. This particular diagram illustrates the states of an activity. The same basic states apply to the lifecycle of a fragment.
You usually request a ViewModel
the first time the system calls an activity object's onCreate()
method. The system may call onCreate()
several times throughout the existence of an activity, such as when a device screen is rotated. The ViewModel
exists from when you first request a ViewModel
until the activity is finished and destroyed.
Clearing ViewModel dependencies
The ViewModel calls the
onCleared
method when theViewModelStoreOwner
destroys it in the course of its lifecycle. This allows you to clean up any work or dependencies that follows the ViewModel's lifecycle.
The following example shows an alternative to viewModelScope
. viewModelScope
is a built-in CoroutineScope
that automatically follows the ViewModel's lifecycle. The ViewModel uses it to trigger business-related operations. If you want to use a custom scope instead of viewModelScope
for easier testing, the ViewModel can receive a CoroutineScope
as a dependency in its constructor. When the ViewModelStoreOwner
clears the ViewModel at the end of its lifecycle, the ViewModel also cancels the CoroutineScope
.
viewModelScope
is a build-inCoroutineScope
that automatically follows the ViewModel's lifecycle.
class MyViewModel(
private val coroutineScope: CoroutineScope =
CoroutineScope(SupervisorJob() + Dispatchers.Main.immediate)
) : ViewModel() {
// Other ViewModel logic ...
override fun onCleared() {
coroutineScope.cancel()
}
}
From lifecycle version 2.5 and above, you can pass one or more Closeable
objects to the ViewModel's constructor that automatically closes when the ViewModel instance is cleared.
class CloseableCoroutineScope(
context: CoroutineContext = SupervisorJob() + Dispatchers.Main.immediate
) : Closeable, CoroutineScope {
override val coroutineContext: CoroutineContext = context
override fun close() {
coroutineContext.cancel()
}
}
class MyViewModel(
private val coroutineScope: CoroutineScope = CloseableCoroutineScope()
) : ViewModel(coroutineScope) {
// Other ViewModel logic ...
}
For managing data within the lifecycle, you can employ either CoroutineScope or closable objects.
Best practices
The following are several key best practices you should follow when implementing ViewModel:
Use at screen level: Because of their scoping, use ViewModels as implementation details of a screen level state holder. Don't use them as state holders of reusable UI components such as chip groups or forms. Otherwise, you'd get the same ViewModel instance in different usages of the same UI component under the same ViewModelStoreOwner.
Generic at accommodate any UI: ViewModels shouldn't know about the UI implementation details. Keep the names of the methods the ViewModel API exposes and those of the UI state fields as generic as possible. In this way, your ViewModel can accommodate any type of UI: a mobile phone, foldable, tablet, or even a Chromebook!
Don't hold references of lifecycle-related APIs: As they can potentially live longer than the
ViewModelStoreOwner
, ViewModels shouldn't hold any references of lifecycle-related APIs such as theContext
orResources
to prevent memory leaks.Don't pass them around: Don't pass ViewModels to other classes, functions or other UI components. Because the platform manages them, you should keep them as close to it as you can. Close to your Activity, fragment, or screen level composable function. This prevents lower level components from accessing more data and logic than they need.
Further information
As your data grows more complex, you might choose to have a separate class just to load the data. The purpose of ViewModel
is to encapsulate the data for a UI controller to let the data survive configuration changes. For information about how to load, persist, and manage data across configuration changes, see Saved UI States.
References
https://developer.android.com/topic/libraries/architecture/viewmodel#recommendations-link