While building apps i have often incorporate ViewModels without fully grasping their intricacies. I made some common mistakes, like creating ViewModel instances within a remember block and such silly mistakes,but this journey has prompted me to dig a little deeper into understanding ViewModels and their internal workings.
The primary questions we are going to address are as follows:
What is a ViewModel?
How to create a ViewModel?
What is the difference between ViewModel and AndroidViewModel?
What is a ViewModelFactory?
What is viewModelScope?
When is the ViewModel dropped by the ViewModelStore?
What is onCleared() in the ViewModel ?
lets begin our journey....
What is a ViewModel?
ViewModel is a simple class that is part of the Android Architecture Components and its primary purpose is to store and manage UI-related data in a lifecycle-conscious manner caching its value during the lifecycle of the activity It helps us to avoid loading data again due to configuration changes. do note here the lifecycle-conscious manner means that the associated ViewModel is conscious of the lifecycle of the Activity which helps it to determine whether it should stop running tasks or start the cleanup.
Hold on, if the ViewModel is lifecycle-conscious and it gets destroyed as soon as the associated Activity is destroyed, how are the values persisted? how does it escape the destruction during configuration changes? I had the same doubt so it seems the right question I must be asking is...
How to create a ViewModel?
There are 6 different ways to create the ViewModel and I will try to explain them:
val myVM1 = ViewModelProvider(this, ViewModelProvider.NewInstanceFactory()).get(MyViewModel::class.java)
val myVM2 = ViewModelProvider.AndroidViewModelFactory(this.application).create(MyViewModel::class.java)
val myVM3 = ViewModelProvider(this).get(MyViewModel::class.java)
val myVM4: MyViewModel by viewModels()
val myVM5 by viewModels()
val myVM6: MyViewModel by viewModels { MyViewModelFactory(application, "param1", "param2") }
While it looks like a bit too much, they can be easily separated into three categories and they are:
- The viewModel with multiple parameter and no parameters(1,2,6 vs 3,4,5).
- The ViewModel initialization with lazy loading and without lazy loading (4,5,6 vs 1,2,3)
- The ViewModels that can be defined at a global level and the ones that need to be manually created in the onCreate block (4,5,6 vs 1,2,3)
Did you also notice that there is a new guest besides our ViewModel? the AndroidViewModel, I'm sure you did, so what is the difference between them? and also what is a ViewModel Factory ?
What is the difference between ViewModel and AndroidViewModel?
Simply, the AndroidViewModle needs an Application Context as a parameter with which it can be used effectively within the ViewModel for database initialization or other similar use-cases.
What is a ViewModelFactory?
A ViewModel Factory is a mechanism in Android used for creating or instantiating ViewModel instances, especially when additional parameters are required during the ViewModel's creation. how to create them here is a sample of creating a custom ViewModel factory.
class MyViewModelFactory(private val someParameter: String) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MyViewModel::class.java)) {
return MyViewModel(someParameter) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
and we can use this custom factory while passing the ViewModel Factory to a delegate or otherwise, as shown below
val myViewModel = ViewModelProvider(this, MyViewModelFactory("parameter")).get(MyViewModel::class.java)
I hope you got the gist of the ViewModelFactory, ok getting back to our initial question...
How do the values persist during configuration changes?
The Magician here is the ViewModelStore interface that is implemented by the ComponentActivity note this stays higher than the activity layer so it can effectively store the ViewModels in a mutableHashMap and take care of retaining the ViewModels until the Application itself is destroyed since the view model store create a new instance only if the store does not have the copy of the instance already, but do make sure that you are not creating a new instance of the ViewModel while re-creating you Activity.
What is viewModelScope?
viewModelScope is a CoroutineScope tied to the ViewModel's lifecycle, which means that they are automatically canceled when the ViewModel is no longer in use, preventing memory leaks. It is completely aware of the ViewModel's lifecycle, ensuring that coroutines are active only when the ViewModel is in a valid state so it is typically used within ViewModel classes for launching coroutines associated with ViewModel-specific tasks.
While you can use this Coroutine Scope for network calls, DB Operations, or other Long running background operations such as image processing or others it is NOT recommended for UI-related logic (I will talk about it more below) or Global or App Wide operation that is beyond the scope of single ViewModel.
The "not recommended for UI related logics" might be a little confusing so what I meant is that avoid invoking snack bars, toasts, dialogs, etc which needs a scope but must be dealt with within the composable. a third role for the ViewModelScope would be using it for background tasks, data loading, or other asynchronous operations that are not directly tied to the user interface.
When is the ViewModel dropped by the ViewModelStore?
If the associated UI component (Activity or Fragment) is completely destroyed (for example, due to the user navigating away from an Activity), the ViewModel might be cleared to release resources. The decision to clear a ViewModel is made by the Android system based on the lifecycle of the UI component. The lifecycle of a ViewModel is tied to the lifecycle of the associated UI component. When the UI component is no longer needed, the ViewModel is typically cleared. For example, if an Activity is finished, its associated ViewModel is likely to be cleared.
What is onCleared() in the ViewModel ?
onCleared method provides a place to release resources or perform any necessary cleanup, such as canceling an active coroutine Job. do note this method is called by the framework, ensuring that you have an opportunity to release resources properly and avoid memory leaks.
And now, let's look at some best practices...
ViewModel Best Practices
-
Separation of Concerns:
Practice: Keep your ViewModels lightweight, focusing on managing UI-related data and logic.
Why: Promotes a clear separation of concerns and maintains a more modular codebase.
-
Immutable State:
Practice: Design ViewModel properties as immutable to ensure predictable behavior.
Why: Immutable state makes it easier to reason about the flow of data and simplifies testing.
-
ViewModelScope with Coroutines:
Practice: Use viewModelScope for coroutine scopes within ViewModels.
Why: Ties coroutines to the ViewModel's lifecycle, ensuring proper cancellation and avoiding potential memory leaks.
-
LiveData and StateFlow:
Practice: For state management, consider using LiveData or StateFlow with Compose.
Why: LiveData provides lifecycle-aware observation, while StateFlow is designed for reactive state updates in Compose.
-
Sharing Data Between Composables:
Practice: Use ViewModel to share data between composables within the same UI hierarchy.
Why: Simplifies communication and state sharing between different parts of your UI.
-
ViewModel Initialization:
Practice: Follow consistent patterns for ViewModel initialization in Compose, considering the different creation methods.
Why : Consistency improves code readability and maintainability.
-
ViewModel Factory:
Practice: Utilize ViewModelFactory when creating ViewModels with dependencies (usually taken care by DI's).
Why : Facilitates dependency injection and allows customization of ViewModel creation logic.
-
Testing ViewModels:
Practice: Write unit tests for your ViewModels, especially testing the logic and interactions without relying on the UI.
Why : Ensures the correctness of your business logic and enhances the reliability of your application.
-
Avoiding UI-Related Logic:
Practice: Avoid placing UI-related logic directly in the ViewModel
Why : Keeps the ViewModel focused on managing data, making it more reusable and testable.
-
ViewModels in Navigation:
Practice: Consider using navigation components along with ViewModels for handling navigation-related logic.
Why : Helps manage the navigation state and simplifies the coordination of UI transitions.
-
ViewModel Cleanup:
Practice: Leverage the onCleared method for cleaning up resources when the ViewModel is no longer needed.
Why : Prevents memory leaks by releasing resources tied to the ViewModel's lifecycle.
-
Data Persistence with ViewModel:
Practice: Use ViewModel to manage and persist UI-related data across configuration changes.
Why : Ensures data continuity during configuration changes, providing a seamless user experience.
A typical ViewModel will look something like this, along with this we may also have Status State which keeps track of the content status for example Initial, Loading, Content, Empty, or Error but since it outside this scope am not including that in this sample ViewModel:
data class DiceUiState(
val firstDieValue: Int? = null,
val secondDieValue: Int? = null,
val numberOfRolls: Int = 0)
class DiceRollViewModel : ViewModel() {
//Job object for effective cancellation
private var job: Job? = null
//Private mutable state helps to manage the value but is not exposed
private val _uiState = MutableStateFlow(DiceUiState())
//immutable state that is exposed to the UI
val uiState: StateFlow<DiceUiState> = _uiState.asStateFlow()
// Handle business logic
fun rollDice() {
_uiState.update { currentState ->
currentState.copy(
firstDieValue = Random.nextInt(from = 1, until = 7),
secondDieValue = Random.nextInt(from = 1, until = 7),
numberOfRolls = currentState.numberOfRolls + 1,
)
}
}
fun someNetWorkCall(){
job = CoroutineScope(Dispatchers.IO).launch {
delay(5000)
}
}
...
override fun onCleared() {
super.onCleared()
// Release resources or perform cleanup when the ViewModel is about to be destroyed
// For example, cancel the ongoing coroutine job if any
job?.cancel()
}
}
The only difference using the AndroidViewModel will be we have to pass the application context as an extra parameter and it can be effectively used if it is required, to add do note that while using an effective DI it will take care of the creation of viewmodel and passing of the dependencies and other repetitive tasks, I just mentioned it here just so I understand what actually happens behind the scenes.
Official Documentation and the class structure is available here:
I Hope you have found some value in this article, if you have any inputs or doubts please do feel free to add a comment below.
Comments
Post a Comment