Skip to main content

Delving Deeper into ViewModel - AndroidNotes

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:

  1. What is a ViewModel?

  2. How to create a ViewModel?

  3. What is the difference between ViewModel and AndroidViewModel?

  4. What is a ViewModelFactory?

  5. What is viewModelScope?

  6. When is the ViewModel dropped by the ViewModelStore?

  7. 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)

A broader analysis on this is available here, please do check it out later Link

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

Continue reading my other posts

Chocolate Feast - Problem Solving - Hacker Rank Solution.

The expectation is to find the total number of choclate one can consume by taking full advantage of the offer, Here there are 3 inputs n which holds the value of initial amount of money for buying choclate, c is the cost price of each candy if paid by cash and m is the exchange rate for the candy. Inputs n Initial cash to buy candy. c Coast of each candy if paid by cas.h m Exchange rate for a new candy in offer. The initial count of choclate will be the cash / coast and the wrappers in hand will be the same value of choclate, and from there we loop through until the wrap count is less than the exchange rate, inside the loop the choclate count will still hold the same fourmula as before but divided with exchange rate. The wrap count is the tricky part... the wrap will be wrap/ exchange rate(the no. choclate) + the remainder of this division(THIS IS VERY IMPORTANT) because for example if the count of wrapper is 3 and the exchange rate is 2 you can only buy 1 c

Designer PDF Viewer - HackerRank Problems

Difficulty: EASY Problem : The objective here is to find the size of the highlighted area, and we are given the size's of all the alphabets, we have to find the largest alphabet in the highlighted word and then calculate the size of the rectangle so if the tallest character is 3 then the size of the box will be 3 * number of characters given. Visual representation of the selection : abc def ghij Inputs An array with the sizes of all alphabets a-z in order. A String of highlighted words. Important points to note The array which holds the height of each character in ascending order which means the arrays 0th index will have the height of a 1st index will have the height of b and so on and so forth in the end the hight of z will be there so it's easy to locate each character. A String with the highlighted word. This means we have got the characters inside the rectangle, all we have to find is