12.
MVVM Sample with Android Architecture Components
Written by Aldo Olivares
In the previous chapter, you learned how MVVM works by understanding how the Model, View and ViewModel interact with each other, and about their responsibilities and limitations.
In this chapter, you are going to use your newly acquired knowledge to rebuild your Movies app to use MVVM by integrating the ViewModel and LiveData components from the Android Architecture Components or AAC.
By the end of this chapter, you will have learned:
- How to migrate an app from the MVC architecture to the MVVM architecture.
- How to integrate the ViewModel with the View layer of your apps.
- How to integrate your Models with your ViewModels.
- How to create a centralized repository for your data sources.
- How to use LiveData to work with asynchronous responses from webservices or APIs.
- And much more!
Getting started
Start by opening the starter project for this chapter. If you haven’t done so already, take some time to familiarize yourself with the code.
Note: You may notice that the starter project looks a little different than the one from previous chapters. Don’t worry, this is intended to give you a head start for this chapter and we will explore it shortly.
The data package has three packages related to the backend of your app:
- The db package contains the files required for your Room database: the MovieDatabase.kt and the MovieDao.kt files.
- The model package contains the models for your app: the Movie model and the MovieResponse model.
- The net package contains the files required by Retrofit to communicate with the TMDB web service: MoviesAPI.kt and RetrofitClient.kt.
Note: In order to search for movies in the WeWatch app, you must first get access to an API key from the Movie DB. To get your API own key, sign up for an account at www.themoviedb.org. Then, navigate to your account settings on the website, view your settings for the API, and register for a developer API key. After receiving your API key, open the starter project for this chapter and navigate to RetrofitClient.kt. There, you can replace the existing value for
API_KEYwith your own.
The view package contains three packages related to the front end of your app such as the Activities and Adapters.
Take all the time you need to familiarize yourself with the project. You will be spending a lot of time on each of the files.
Once you are ready, build and run the app on a device or emulator to see it in action. You should now see the basic app running.
Current architecture layers
Before making any change to the code of your app, take a quick look at the current architecture, just to refresh:
It’s pretty easy to see what’s going on: Activities and Fragments communicate with the data store directly. While this architecture is quite easy to understand (and works just fine), there are some disadvantages:
- Your views are individually interacting directly with the TMDB API and with your Room database. There should be a centralized repository to get information from all your backends, including your APIs and your DB.
- Your current structure is violating the single responsibility principle. Views are doing too much work; they should only be in charge of displaying the UI and receiving events from the user.
Your mission, should you choose to accept it, will be to fix each of these flaws with the MVVM architecture pattern by adding ViewModels and LiveData to the mix. At the end, you will compare the old architecture with the new one to see how things have improved.
Creating a movie repository
You will start by creating a centralized repository to retrieve movies for your app, both from the TMDB API and from your Room database.
Start by opening build.gradle in your app directory. Add the following line inside the dependencies block:
def lifecycle_version = '2.0.0-rc01'
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
The code above adds the lifecycle part of the Android Architecture Components to your dependencies, including ViewModel and LiveData.
Click the Sync Now button that should have appeared at the top of the editor and wait until Android Studio has finished syncing your project.
Under the data package, create a new Kotlin class and name it MovieRepositoryImpl.
Replace the autogenerated class with the following:
class MovieRepositoryImpl : MovieRepository {
//1
private val movieDao: MovieDao = db.movieDao()
private val retrofitClient = RetrofitClient()
private val allMovies: LiveData<List<Movie>>
//2
init {
allMovies = movieDao.getAll()
}
//3
override fun deleteMovie(movie: Movie) {
thread {
db.movieDao().delete(movie.id)
}
}
//4
override fun getSavedMovies() = allMovies
//5
override fun saveMovie(movie: Movie) {
thread {
movieDao.insert(movie)
}
}
//6
override fun searchMovies(query: String): LiveData<List<Movie>?> {
val data = MutableLiveData<List<Movie>>()
retrofitClient.searchMovies(query).enqueue(object : Callback<MoviesResponse> {
override fun onFailure(call: Call<MoviesResponse>, t: Throwable) {
data.value = null
Log.d(this.javaClass.simpleName, "Failure")
}
override fun onResponse(call: Call<MoviesResponse>, response: Response<MoviesResponse>) {
data.value = response.body()?.results
Log.d(this.javaClass.simpleName, "Response: ${response.body()?.results}")
}
})
return data
}
}
Note: Whenever you add code, make sure to import the appropriate packages by pressing Alt + Enter on Windows or Option + Enter on Mac.
Taking each commented section, in turn:
-
Here, you create the
movieDaoandretrofitClientproperties. TheretrofitClientinstance is initialized immediately since it does not require the app context. -
Next, you initialize the
movieDaoproperty by creating aMovieDatabaseinstance. -
getSavedMovie()returns a list of all the movies stored in your Room database. -
saveMovie()takes a movie as a parameter and uses theinsert()method of themovieDaoto save it in your database. -
updateMovie()takes a movie parameter and uses theupdateMovie()method of yourmovieDaoto update the appropriate record in your database. -
Finally,
deleteWatchedMovies()deletes all the movies in your database whosewatchedattribute is equal totrue.
You may notice that the saveMovie() and updateMovie() methods are using the thread() method from Kotlin’s standard library to create a separate thread and execute database tasks. This is needed since database operations can take a long time to run and may block the main thread in which your app is executing. Since UI operations happen on the main thread, bogging it down with non-UI workloads will make the UI stutter and become unresponsive.
Note: In the above code, you use Kotlin’s
threadmethod to create a separate thread rather than an async task for simplicity. This way, you don’t have to make your MovieRepository class extend the AsyncTask class and implement different methods.
The last step in creating the movie repository is to make it available to your other classes with a single-entry point.
Open App.kt inside the root directory of your app and add the following method just below onCreate():
fun getMovieRepository(): MovieRepository = MovieRepository(this)
The above method returns an instance of your MovieRepository. Since your MovieDatabase needs a reference to your application context, the App file is the best place to create an accessor method.
And that’s it! Now that you have created your movie repository and made it available through your getMovieRepository() method, it is time to create your ViewModels.
Creating ViewModels
While it is possible to access the movie repository from your views, it is generally considered bad practice to have your Activities or Fragments communicate directly to your backend.
Creating your movie repository was just the first part of migrating your app. In the MVVM architecture, ViewModels are in charge of receiving requests from your Views, communicating those requests to your Models and updating your backend accordingly.
You can create your own ViewModel classes from scratch. In fact, this is what developers used to do before Google introduced the Android Architecture Components. But now, you have an easy and consistent way of creating the ViewModels for our Views: The ViewModel Architecture Component.
According to the official documentation:
The ViewModel class is designed to store and manage UI-related data in a lifecycle conscious way. The ViewModel class allows data to survive configuration changes such as screen rotations.
No way! This is exactly what you need for your app.
Create a new package under the root directory and name it viewmodel
Create a new class under the viewmodel package and name it AddViewModel.
Replace the autogenerated class with the following code:
//1
class AddViewModel(private val repository: MovieRepository = MovieRepositoryImpl()): ViewModel() {
//2
fun saveMovie(movie: Movie) {
repository.saveMovie(movie)
}
}
Taking each commented section in turn:
-
AddViewModelextends from theViewModelclass. If you were to take a look at the documentation forViewModel, you’d find that there is no need to override any method to make your data survive configuration changes. Everything is already taken care of for you. TheMovieRepositoryis immediately initialized using the constructor. -
saveMovie()uses your movie repository to save the movie passed as a reference to the database.
Note: The only difference between the
ViewModeland theAndroidViewModelclass is that the latter depends on your app’s context. This is useful when working with other libraries, such as Room, but it also makes your app harder to test. We will talk more about this in the MVVM Testing chapter.
Now that your ViewModel is ready, it’s time to use it.
Open AddMovieActivity.kt and add the following attribute to store a reference to an instance of AddMovieViewModel:
private lateinit var viewModel: AddViewModel
Once you have your attribute add the following code inside the onCreate() method:
viewModel = ViewModelProviders.of(this).get(AddViewModel::class.java)
ViewModelProviders is a special class that returns an existing ViewModel or creates a new one while the scope of a given Activity/Fragment is alive. In this case, since you are passing a reference to your AddMovieActivity, it will create a new AddViewModel that will stay alive during the whole lifecycle of your AddMovieActivity activity.
The only step left is to use your ViewModel to save a movie when the user presses the Save Movie button.
Locate the addMovieClicked() method and add the following code:
fun addMovieClicked(view: View) {
if (titleEditText.text.toString().isNotBlank()) {
viewModel.saveMovie(Movie(
title = titleEditText.text.toString(),
releaseDate = yearEditText.text.toString()))
finish()
} else {
showMessage(getString(R.string.enter_title))
}
}
As you can see, there is no need to create a thread inside your Fragment since your MovieRepository is already creating one each time saveMovie() is called. This helps you avoid code duplication inside your Views.
Build and Run the app. Try adding a movie to verify everything is working properly:
Before creating the next ViewModels, you will need to learn about LiveData.
Using LiveData with ViewModels
In an earlier section, you added a method named searchMovie() to your repository which returned a LiveData list of movies, but what is LiveData?
LiveData is a data holder class, just like a List or a HashMap, that can be observed for changes within a given lifecycle. This basically means that you can attach an Observer that will be notified about any modification on the wrapped data.
For example, say that you want to retrieve a list of users from your database with a method like the following:
fun getUsers(): List<User> {
return userDao().getAll()
}
There are two problems with the above approach. First, this method is passive; it only retrieves the list of all users in the database when it is explicitly called upon to do so. So, if you were to use it to back a list UI, you would have to call it every time you added, inserted or deleted a user.
Second, the method is synchronous; it blocks the calling thread until the database query is finished. if you execute long-running tasks in the UI thread, your app could be stopped by the operating system and the user would get an Application Not Responding Error or ANR
To solve this problem you could use LiveData to wrap your list of users:
fun getUsers(): LiveData<List<User>> {
return users
}
Then, observe for any changes with an Observer like below:
getUsers().observe(this, Observer { users ->
//Update UI with list of users
})
This approach is much better since the observer will notify any consumers of data changes as they happen, removing the need to respond to those changes manually.
With the above in mind, you will use LiveData’s powers to observe your Room database and your Retrofit callbacks.
Create a new class under the viewmodel package and name it MainViewModel.
Replace the code inside with the following:
class MainViewModel(private val repository: MovieRepository = MovieRepositoryImpl()) : ViewModel() {
//1
private val allMovies = MediatorLiveData<List<Movie>>()
//2
init {
getAllMovies()
}
//3
fun getSavedMovies() = allMovies
//4
private fun getAllMovies() {
allMovies.addSource(repository.getSavedMovies()) { movies ->
allMovies.postValue(movies)
}
}
//5
fun deleteSavedMovies(movie: Movie) {
repository.deleteMovie(movie)
}
}
Step by step:
- First, you create the
repositoryand theallMoviesproperties. You may have noticed that theallMoviesproperty is aMediatorLiveDatatype.MediatorLiveDatais a subclass ofLiveDatathat can hold data from different sources. It can also react toonChangedevents fromLiveDataobjects. - Next, you call the
getAllMovies()method as soon as theMainViewModelclass is initialized. -
getSavedMovies()returns a LiveData list of movies stored in yourallMoviesproperty. -
getAllMovies()sets the datasource ofallMoviesfromMovieRepository. It fetches the list of movies by executingrepository.getSavedMovies()and posting the value toallMovies. -
deleteSavedMovies()deletes the movie passed as a parameter using thedeleteMovie()method of yourMovieRepository.
Your ViewModel is now ready to be used inside your Activity.
Open MainActivity.kt and add the following attribute to hold a reference to your MainViewModel:
private lateinit var viewModel: MainViewModel
Initialize it inside the onCreate() method just like you did in AddMovieViewModel:
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
Now that your viewModel has been initialized, it’s time to use it.
Add the following code below the line you just added inside onCreate():
showLoading()
viewModel.getSavedMovies().observe(this, Observer { movies ->
hideLoading()
movies?.let {
adapter.setMovies(movies)
}
})
The above code uses the getSavedMovies() method from your ViewModel to retrieve a LiveData list of movies. The observe() method attaches the Observer passed as a parameter to the observers list of your LiveData object. Once the movies have been retrieved from your database, your observer’s callback is executed and the adapter receives the list of movies to be displayed in your recyclerView.
You might also notice that the observe method passes your Activity, an instance of LifecycleOwner, as the first parameter. By doing so, the observer is bound to the Lifecycle object associated. This basically means three things:
- After the
Lifecycleobject is destroyed, the observer is automatically destroyed. - If the
Lifecycleis inactive, the observer isn’t called, even if your list changes. -
LiveDataobjects, just like yourViewModelobjects, are lifecycle-aware. You can share data between your Activities, Fragments or even services.
Now, add the following inside deleteMoviesClicked():
for (movie in adapter.selectedMovies) {
viewModel.deleteSavedMovies(movie)
}
The MovieList screen of your app is now ready!
Build and run to see it in action:
Creating the SearchViewModel
Before creating your new ViewModel open the MovieRepository.kt file and analyze the searchMovies() method bit by bit:
override fun searchMovies(query: String): LiveData<List<Movie>?> {
//1
val data = MutableLiveData<List<Movie>>()
//2
retrofitClient.searchMovies(query).enqueue(object : Callback<MoviesResponse> {
//3
override fun onFailure(call: Call<MoviesResponse>, t: Throwable) {
data.value = null
Log.d(this.javaClass.simpleName, "Failure")
}
//4
override fun onResponse(call: Call<MoviesResponse>, response: Response<MoviesResponse>) {
data.value = response.body()?.results
Log.d(this.javaClass.simpleName, "Response: ${response.body()?.results}")
}
})
return data
}
Briefly, here’s what’s going on:
- First, you create an empty
MutableLiveDatalist of movies. - Next, you call the
searchMovies()method of your retrofitClient to retrieve movies that match the given query. - The
onFailure()method is triggered if there is a problem with your call to the TMDB API. Appropriate error handling has been omitted here for simplicity. - Once there is a successful response from the TMDB API, your movie list is set by using the
setValue()method of yourMutableLiveDataclass.
You might notice that you are using MutableLiveData instead of LiveData. MutableLiveData is a LiveData subclass that exposes two methods: setValue() and postValue():
-
setValue(): Sets the value of your data from the main thread. -
postValue(): Adds a task to the main thread to set the value of your data.
In short: Use the setValue() method if you are on the main thread and the postValue() method if you are on a background thread.
Now, it’s time to create your ViewModel.
Create a new class under the viewmodel package and name it SearchViewModel.
Replace the auto-generated code inside the class with the following:
class SearchViewModel(private val repository: MovieRepository = MovieRepositoryImpl()): ViewModel() {
fun searchMovie(query: String): LiveData<List<Movie>?> {
return repository.searchMovies(query)
}
fun saveMovie(movie: Movie) {
repository.saveMovie(movie)
}
}
The code above creates your repository and creates two methods to save and retrieve movies: the searchMovie() method and the saveMovie() method.
Now that your ViewModel is ready, the only thing left is to use it inside your Activity.
Open SearchMovieActivity.kt and add a property to store an instance of your SearchViewModel class:
private lateinit var viewModel: SearchViewModel
Initialize your attribute inside onCreate():
viewModel = ViewModelProviders.of(this).get(SearchViewModel::class.java)
Create a new searchMovie() method:
private fun searchMovie() {
showLoading()
viewModel.searchMovie(title).observe(this, Observer { movies ->
hideLoading()
if (movies == null) {
showMessage()
} else {
adapter.setMovies(movies)
}
})
}
The above uses the searchMovie() method of your ViewModel to retrieve the list of movies from the TMDB API. Once the list of movies have been retrieved, your observer’s callback is executed and the adapter is attached to your RecyclerView.
Go to the displayConfirmation() method and add the following code inside the snackbar action:
viewModel.saveMovie(movie)
The code above uses the saveMovie() method of your MovieRepository to save the movie passed as a parameter and returns to the MainActivity.
Finally, call your searchMovie() method as soon as the SearchMovieActivity is created by adding the following line inside onCreate():
searchMovie()
And also use it inside the showMessage() method by adding this code inside your snackbar action:
searchMovie()
Build and Run your app one last time to test your changes:
MVVM architecture
At the beginning of this chapter, you saw the current architecture of your app without MVVM.
Your new architecture has the following advantages:
- Your Views only have one job: interacting with the user and displaying the UI.
- Your ViewModels handle all the interaction between your Models and your Views.
- You now have a centralized repository for your two different backend endpoints: your local database and your external API.
- All your classes respect the single responsibility principle.
Key points
- The
ViewModelclass is designed to store and manage UI-related data in a lifecycle-aware way. - The
ViewModelclass allows data to survive configuration changes, such as screen rotations. -
LiveDatais a data holder class, just like a List or a HashMap, that can be observed for any changes within a given lifecycle. - Having a robust architecture like MVVM makes your code scalable and easy to maintain.
Where to go from here?
Although refactoring an app might seem like a daunting task at first, it pays off in the long run. Having a robust architecture like MVVM makes your code scalable and easy to maintain.
In the next chapter, you will learn how to further improve your code by integrating the Data Binding library to bind your UI components in your layouts to your data sources.