This is part 2 of our 3-part series on implementing MVVM (Model-View-ViewModel) using Google’s ViewModel and LiveData architecture components. If you are not familiar with these 2 architecture components, check out part 1.
In this post, we will look at a very simple project that currently uses the MVP (Model-View-Presenter) architecture and Coroutines, and convert it to MVVM using ViewModel and LiveData. If you’re not familiar with Kotlin Coroutines, check out our post on Coroutines first.
The starting point is here.
This is a simple (sample) app that hits a mocky endpoint to get a list of NBA teams, and displays them as a list inside a RecyclerView. The overall structure is very similar to the code from our post on coroutines:
NetworkClient and NetworkAPI are used to retrieve JSON data from the network and to convert it into a list of Team data objects. This data is then displayed in MainActivity using TeamListPresenter. MainActivity implements CoroutineScope, and cancels any running coroutines in onDestroy().
Since ViewModel can survive configuration changes, we now want our view model to implement CoroutineScope (instead of MainActivity). This would ensure that our coroutines do not get canceled even when the screen rotates and our activity is recreated. Let’s set up our ViewModel then:
Next, we take a look at the View-Presenter contract:
There is a loadTeams() method in the presenter which would now belong in the view model. In the view, we have three methods that control three different parts of its state: showLoading() shows a loading animation when waiting for the network call; showTeamList(teams:ListTeam) shows the list of teams if the call returns successfully; showError() displays an error message if something goes wrong. In MVVM, it’s often helpful to think of the view model as being dictated by the data to be displayed in the view. Since the view state has 3 different parts, we’ll go with a simple approach and make 3 LiveData objects inside our view model, each controlling one part of the state:
We want to expose the read-only version of the LiveDatas to the activity, and we do that using three public functions. Finally, our ViewModel is as follows:
Now we move on to the loadTeams() method, which previously lived in the Presenter, like so:
The same logic still works in the view model, but instead of calling methods on the view, we simply update the LiveData values. The activity should update the UI based on changes to the LiveDatas it’s observing. Inside the view model, we can call loadTeams() when teams is initialized.
Here we add a finally clause to hide the loading animation, regardless of the result from the network.
Now we can observe the LiveDatas in the activity. We remove all code related to TeamContract.View and TeamContract.Presenter. Then we get the ViewModel in onCreate() and subscribe to the LiveData:
When the LiveDatas in the view model change, the activity will be notified so it can update the UI accordingly. At this point, we are at the same state as in the MVP version, but using LiveData and ViewModel.
If we run the app now, we’ll see exactly the same UI. However, the benefit of this approach is that if we rotate the device, the list of teams will persist in the view model, and it won’t be re-downloaded from the network. In fact, it’s not just rotation that the view model survives: with Android apps now running on Chromebooks and foldable smartphones officially being supported, the view model should also survive window-resizing configuration changes.
You can check out the final code here.
There’s always room for improvement. As the app grows more complex, so will the view model. For example, we might want to persist our data in the database after obtaining it from the network. We can handle that in the view model, but by doing so, our view model becomes more and more difficult to maintain as it grows. We end up giving too much responsibility to the ViewModel class, which violates the “separation of concerns” principle. In such a scenario, it would be a good idea to extract the data logic out of the view model and delegate the data-fetching process to a repository. Check out a sample implementation in this branch.
Stay tuned for part 3, where will go over how to write tests when using ViewModel and LiveData.
Yawei Li is a Toronto-based Android developer and an avid dog lover.
Shaurya Arora is a Toronto-based Android developer, drummer and prog metal/djent lover.