Home Android & Kotlin Books Advanced Android App Architecture

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_KEY with 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:

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.

def lifecycle_version = '2.0.0-rc01'
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-viewmodel:$lifecycle_version"
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
  }
}
fun getMovieRepository(): MovieRepository = MovieRepository(this)

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.

//1
class AddViewModel(private val repository: MovieRepository = MovieRepositoryImpl()): ViewModel()  {
  //2
  fun saveMovie(movie: Movie) {
    repository.saveMovie(movie)
  }
}
private lateinit var viewModel: AddViewModel
viewModel = ViewModelProviders.of(this).get(AddViewModel::class.java)
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))
  }
}

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?

fun getUsers(): List<User> {
  return userDao().getAll()
}
fun getUsers(): LiveData<List<User>> {
  return users
}
getUsers().observe(this, Observer { users ->
  //Update UI with list of users
})
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)
  }
}
private lateinit var viewModel: MainViewModel
viewModel = ViewModelProviders.of(this).get(MainViewModel::class.java)
showLoading()
viewModel.getSavedMovies().observe(this, Observer { movies ->
  hideLoading()
  movies?.let {
    adapter.setMovies(movies)
  }
})
for (movie in adapter.selectedMovies) {
  viewModel.deleteSavedMovies(movie)
}

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
}
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)
  }
}
private lateinit var viewModel: SearchViewModel
viewModel = ViewModelProviders.of(this).get(SearchViewModel::class.java)
private fun searchMovie() {
  showLoading()
  viewModel.searchMovie(title).observe(this, Observer { movies ->
    hideLoading()
    if (movies == null) {
      showMessage()
    } else {
      adapter.setMovies(movies)
    }
  })
}
viewModel.saveMovie(movie)
searchMovie()
searchMovie()

MVVM architecture

At the beginning of this chapter, you saw the current architecture of your app without MVVM.

Key points

  • The ViewModel class is designed to store and manage UI-related data in a lifecycle-aware way.
  • The ViewModel class allows data to survive configuration changes, such as screen rotations.
  • LiveData is 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.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as obfuscated text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.