Ultimate Guideline on How to Use ViewModel Design Pattern

Since the design patterns entered Android development world, the application drastically adapted to the new trend. Two of which this article is going to be about are Repository and ViewModel design patterns. While Repositories control where the data flow is emitted from (either database or network), ViewModel is mainly responsible for keeping the UI state. These design patterns are not only improved Android projects code quality on performance as well as readability point of views, but also increased test coverage standpoint. Since the unit test will not be required to have Context to be mocked anymore, the developers can write more smaller and meaningful test cases.

This article is written under the assumption of having basic knowledge of design patterns as well as some core Android best practices, such as how to use ViewModel, Repository, and UseCases in an Android projects. If you are new in Android programming, I would suggest going over some basic courses in here. And if you are new in design patterns, you can check out this friendly guide book.

Even though ViewModels are not naturally aware of Lifecycle callback methods, the projects I worked on almost always had some work around on that. Either by calling public functions from activity’s onResume/onPause, or directly inheriting ViewModel with LifecycleObserver. ViewModel is meant to save developers from Lifecycle callback hell. When somehow ViewModel is aware of these callbacks, then the developers would still need to consider where to subscribe, and dispose.

The Crucial Blueprint of Android Architecture

Interesting fact about the human brain is that, despite it is capable of handling multiple tasks in manner of nano seconds, learning the impossible, and inventing the physics that we know today, the human brain is not very good at understanding details right away. Thus, it is very crucial to explain something in a high level first, so that the brain can focus more on details. In this concept, the high-level explanation draws the lines of a box for the brain to learn more in depth and focus within the box rather than thinking outside the box.

In Android world, the box is our business logic, and the high-level explanations that we can use are our package names, class names, and function names. When the code is not clean, in the sense of naming, or packaging, it takes tremendous amount of time to understand what the code is doing. Almost every developer already faced or will face with this issue. The fact that we do not understand our own code after a year is the proof of this.

In this article, we will talk about how we can use ViewModel and Repository design patterns into our advantage to hide any details of our code including lifecycle changes in the UI. Lifecycle in Android is very complex, and it varies from Activity to Fragment, from Compose to ViewModel. However, we can still have ultimate blueprint of our application in 3 simple steps.

Repository Pattern Setup

At first, repositories might not look to have much effect on the codebase, but they do. In Android development world or even mobile app in general, developers do not deal with one source or API. We usually have many sources or APIs to retrieve the data. When everything is built upon the ViewModel to combine all those sources to show meaningful information to the user, then it will not be the cleanest code even though recommended design pattern is used. Moreover, what if you want to share that logic with another ViewModel?

A Repository provides a Single Source of Truth for the application. It simply hides the details of where the data flow comes from. This can be either from a network call (API), or it can be from persistent storage (mostly Room DB). The key here is that, only the repository should know about when to call database, or network. And, yes, you guessed right. We cannot pass forceNetwork boolean parameter when we want the data only coming from network. That still defeats the true purpose of Repository pattern.

For truly implementing Single Source of Truth principle, we can simply accept one concept, which is that database data first, then network data if needed. Basically, this concept tells us to first draw the UI with the cache data. In the meantime, the network call can be initiated if the cache data we have is invalid. Invalid data can be declared on various different variables, such as timestamp, or a flag that API provides, or even combination of both. Regardless how invalid data is declared, the logic itself should live in Repository, and only controlled by the Repository itself.

class NewsRepository(
    private val apiClient: NewApiClient,
    private val newsDao: NewsDataAccessObject
) {
    
    private val newsFlow by lazy { 
        newDao.getCachedNews()
            .flatMapLatest { news ->
                if (isCacheDataValid(news)) emptyFlow()
                else apiClient.fetchLatestNews()
                    .onEach { newsDao.saveLatestNews(it) }
                    .onStart { emit(news) }
            }
            .distinctUntilChange()
    }

    operator fun invoke(): Flow<News> = newsFlow
}
Kotlin

As you can see above, right after we fetch cached news from Room database, we check if the news we had are still valid or not. If it is valid, there is no further operation is required, so that we can have an empty flow instead. However, if the cached news are invalid, then we fetch the latest news from retrofit API client. Of course, as soon as we got the latest news, we need to save it into our database.

Now, you should be asking, what is onStart doing here? That, in fact, plays a great role here. Remeber that we mentioned database data first above. onStart operation will emit cached news first and will allow the UI to be drawn with the cache value. Later then, when the API call is successful, we will have updated news.

UseCase(s) for Business Requirements

In 1992, Ivar Jacobson co-authored a book called, Object-Oriented Software Engineering: A Use Case driven approach, which laid foundation of object-oriented software engineering methodology to design a system that can easily be applied to business requirements. Main concept is to capture business requirements and simply design your system depending on those requirements.

In Android development world, a use case means a domain layer that holds the business logic, such as getting latest news data, refreshing news data, or login/logout user. A use case should not care about where the data is coming from (repository), or who observes the result (UI). It should only hold the business logic that is part of a feature. A feature or a ViewModel can hold many UseCases to be built. While this makes business logic more testable, it also protects the most fragile thing to have any harm from any changes within the codebase.

ViewModel architecture

As seen in the diagram, domain layer (UseCase) depends on the data layer (Repository) to have the data flow, and modifies the data depending on the business requirements, and observed by the UI layer. Let’s look at our two examples of a use case;

class GetLatestSportNewsUseCase(
    private val newsRepository: NewsRepository
) {
    
    operator fun invoke() = newsRepository()
        .filter { news -> news.type == NewsType.Sport }
}
Kotlin
class RefreshLatestNews(
    private val apiClient: NewApiClient,
    private val newsDao: NewsDataAccessObject
) {
    
    suspend operator fun invoke(): Unit {
        val news = apiClient.fetchLatestNews()
        newsDao.saveLatestNews(news)
    }
}
Kotlin

RefreshLatestNews? Was not it the repositories responsibility? Yes, but we still have pull to refresh logic for our customers. Technically, it is a feature and a business requirement, so we need to support it without breaking any systematic change. So, we can create a separate UseCase for pull to refresh event. Notice that, it does not return any data, that is because the repositories job to provide the updated data, in this case, news. When we save the latest news data to our Room database, another emission will be triggered from our repository, and all other use cases will benefit from freshly downloaded data, even though they did not asked.

For more on domain layer, please check out this article.

ViewModel the Conquerer

ViewModel is the final piece of the puzzle. So far, we have built our Repository, which is our Single Source of truth, and our business requirements aka. UseCases. Now it is to time to combine them.

There are many thoughts about ViewModels responsibilities. ViewModels are even considered for holding business logic, which might be wrong. ViewModels should be considered as a part of UI layer. When a ViewModel behavior is analyzed internally, it is obvious that a ViewModel can persist after a configuration change. It is because, we used to have savedInstanceState to keep our UI states after a configuration change. The problem with that was, a bundle can only hold so much data that forced developers to call repository over and over again after every configuration changes.

With ViewModel design pattern, we can hold any number of UI states, and they will survive any configuration change because we can retrieve the same instance of ViewModel over and over again until the ViewModelStore (mostly activity or fragment) is destroyed for real. Since a ViewModel’s main job is to hold UI states, it should be considered as part of a UI. That would mean that we can only depend on the things in domain layer, which are our UseCases.

Enough with the explanation, let’s build our sports news page ViewModel.

class SportsNewsPageViewModel(
    private val sportNewsUseCase: GetLatestSportNewsUseCase,
) : ViewModel() {

    val sportNewsState: StateFlow<List<News>> by lazy {
        sportNewsUseCase()
            .stateIn(viewModelScope, SharingStarted.Eagerly, emptyList())
    }
}
Kotlin

Ok, a lot going on here. Let’s break it down.

First of all, we have passed our GetLatestSportNewsUseCase as a parameter to our ViewModel. Then we used our use case to create sportNewsState state flow, where we hold our UI state. In this case, our list of news can be considered as a UI state because we render the UI using this data. Lastly, the tricky part is the stateIn, where we take the normal flow and convert it into StateFlow, which can be observed by the UI without any cost of creating any DB connection.

stateIn will internally collect any emissions coming from the use case and hold it with in a StateFlow. We can declare which scope it needs to be run (viewModelScope – recommended), sharing strategy, and initial value. Well, it makes sense to start with an empty list first, but you might be wondering why we are using Eagerly. Is it a bad practice? Not particularly, especially if it used with in by lazy block. Because of the lazy initialization, we are not start collecting any data until the UI is created. We do not cancel the job later point because we want to emit any emission emitted by the use case until the viewModelScope is cleared.

Ok, let’s add another requirement into our ViewModel, pull-to-refresh.

class SportsNewsPageViewModel(
    private val sportNewsUseCase: GetLatestSportNewsUseCase,
    private val refreshNewsUseCase: RefreshLatestNewsUseCase,
) : ViewModel() {

    val sportNewsState: StateFlow<List<News>> by lazy { ... }
    
    fun onPullToRefresh() {
        viewModelScope.launch {
            refreshNewsUseCase()
        }
    }
}
Kotlin

All we need to do is to add another our UseCase to fetch the news from API client and save it into our Room database. As mentioned above, this use case only has a suspend function and it returns Unit, which we cannot change anything on the UI depending on that response. So, the only way the UI is going to be updated through the StateFlow that we declared above, sportNewsState. Once the new data saved into database, Room will trigger another emission so that we can update our UI without any cost.

Conclusion

In this article, we covered a lot. We talked about Repository pattern, UseCases, and ViewModel, data-domain-UI layers respectively. Repositories are only responsible to decide where the data is coming from either database, or network API while UseCases are holding our business logic even though the business logic as simple as filtering. ViewModels, on the other hand, only cares about the UI state.

More posts are coming. Until then, stay tuned, and happy coding …

Feel free to follow and join my email list at no cost. Don’t miss out — sign up now!

Please check out my earlier blog post for more insights. Happy reading!

The Innovative Fundamentals of Android App Development in 2023

How to learn Android App Development in 2023 – Full Guide For Beginners using Kotlin Language

Ultimate Guideline on How to Use ViewModel Design Pattern

What is the Repository Pattern in Android? A Comprehensive Guide

What is ViewModel in Android? A Good Guide for Beginners

The Innovative Fundamentals of Android App Development in 2023

How to Say Goodbye to Activity Lifecycle and Say Hello to Compose Lifecycle ?

How to Monitor/Observe Network Availability for Android Projects

Exploring Sealed Class vs Enum in Kotlin Which is Best for Your Code?

What is the difference between Functional Programming and OOP?

Memory Leaks in Android Development A Complete Guide

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments
0
Would love your thoughts, please comment.x
()
x