MvRx Android on Autopilot: Getting Started

In this MvRx Android tutorial, you’ll learn how to use this pattern to render the screens of your app based on ViewModels that change state. By Subhrajyoti Sen.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 2 of 2 of this article. Click here to view the first page.

Modeling State

State is the central part of an MvRx project. It contains the data that decides the action the app takes based on different events.

Your first step toward building your movie app is to use states to manage your watchlist. To do this, create a new file called WatchlistState.kt and add the following code to it:

import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized

data class WatchlistState(
   val movies: Async<List<MovieModel>> = Uninitialized
) : MvRxState

WatchlistState extends MvRxState to tell MvRx this class represents a state.

It contains a property called movies that lists all the movies available in the app and has a type of Async<List<MovieModel>> because the data for this property loads asynchronously. You give it an initial type of Uninitialized since it won’t have any data when the user opens the app.

The state should contain only the data crucial to your app’s behavior.

For example, you could also store the list of watchlisted movies in the state. However, since you can derive the same data from the movies list, you don’t need a separate property for the watchlisted movies list.

Connecting the State to the ViewModel

In MvRx, the ViewModel contains the state because of its lifecycle benefits: only the ViewModel can modify the state, and other classes have to use the ViewModel to access the state.

Your next step is to create a connection between the ViewModel and the state.

To do this, create a class called WatchlistViewModel.kt and add the following code to it:

import com.airbnb.mvrx.BaseMvRxViewModel
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.ViewModelContext

class WatchlistViewModel(
    initialState: WatchlistState,
    private val watchlistRepository: WatchlistRepository
) : BaseMvRxViewModel<WatchlistState>(initialState, debugMode = true) {

  companion object : MvRxViewModelFactory<WatchlistViewModel, WatchlistState> {

    override fun create(viewModelContext: ViewModelContext,
                        state: WatchlistState): WatchlistViewModel? {
      val watchlistRepository = 
        viewModelContext.app<WatchlistApp>().watchlistRepository
      return WatchlistViewModel(state, watchlistRepository)
    }
  }
}

In this code, WatchlistViewModel extends BaseMvRxViewModel to specify that this class is a ViewModel that contains a state of type WatchlistState and takes an instance of WatchlistState and WatchlistRepository as constructor parameters.

Since all properties in the WatchlistState class have a default value, the WatchlistViewModel constructs an instance of WatchlistState on its own.

Note that you’ve set the parameter debugMode to true. If debugMode is true, MvRx performs a set of validations to make sure your state management is reliable.

Finally, the companion object MvRxViewModelFactory follows the ViewModelProvider Factory pattern to get an instance of WatchlistRepository and uses it to create an instance of WatchlistViewModel.

Now that the ViewModel and state are ready, it’s time to start observing the state changes in the UI.

Observing State

As you learned earlier, you can only access the state through the ViewModel. In your app, the fragments need instances of the WatchlistViewModel.

To do this, you’ll have to first modify AllMoviesFragment so it can start observing state changes.

Open AllMoviesFragment.kt, and make it extend BaseMvRxFragment as follows:

class AllMoviesFragment : BaseMvRxFragment()

Replace androidx.fragment.app.Fragment with the following:

import com.airbnb.mvrx.BaseMvRxFragment

After you do that, Android Studio will prompt you to implement a method called invalidate().

Do this by adding the following code after onViewCreated():

override fun invalidate() {
}

Whenever there’s a change in the state, it calls invalidate().

Now that you have everything set up, it’s time to put the ViewModel to work!

Using the ViewModel

Add the following code right before onCreate():

private val watchlistViewModel: WatchlistViewModel by activityViewModel()

Next, add the following import:

import com.airbnb.mvrx.activityViewModel

This creates a shared ViewModel that all fragments with the same parent activity can access. activityViewModel is an extension function from MvRx that does the work for you.

Now that you’ve created an instance of the ViewModel, you can use it to access the state in the activity. Since invalidate() is called when the state changes, add the following code inside the invalidate() method:

withState(watchlistViewModel) { state ->
  when (state.movies) {
    // 1
    is Loading -> {
      progress_bar.visibility = View.VISIBLE
      all_movies_recyclerview.visibility = View.GONE
    }
    // 2
    is Success -> {
      progress_bar.visibility = View.GONE
      all_movies_recyclerview.visibility = View.VISIBLE
      movieAdapter.setMovies(state.movies.invoke())
    }
    // 3
    is Fail -> {
      Toast.makeText(
          requireContext(), 
          "Failed to load all movies", 
          Toast.LENGTH_SHORT
      ).show()
    }
  }
}

You’ll need the following imports:

import android.widget.Toast
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.withState

Here what’s going on in the code above:

Modifying the State

Where to Go From Here?

Setting the First State

Modifying Parts of the State

Handling Different States

One more thing, tell the ViewModel to start fetching the list of movies from the repository.

Open WatchlistViewModel.kt, and add the following code right after the class declaration:

Add the corresponding import:

Whenever you create an instance of WatchlistViewModel, it calls this method. Here’s what’s going on in it:

And that’s it! You’ve successfully observed the state in your fragment and updated the UI.

Build and run. Notice that the ProgressBar displays for a few seconds, then a list of movies populates the UI.

App adding movies to the watchlist

When a user clicks on the watchlist icon below the movie’s poster, the app should add the movie to the watchlist. Right now, that doesn’t happen. Your next step is to build that feature.

First, modify WatchlistRepository to add the functionality that adds and removes a movie to the watchlist.

Open WatchlistRepository.kt and add the following methods to the class:

watchlistMovie() takes a movie’s ID, finds it in the movie list, changes the watchlist status to true and returns the movie object. removeMovieFromWatchlist() does the same thing except it changes the watchlist status to false instead.

Now, the ViewModel needs to call these methods of the repository.

Open WatchlistViewModel.kt and make the following code changes:

Add the following line before the init block:

And add the corresponding import:

Add the following function after the init block:

You'll need these imports:

Here what's going on in the code above:

Now, add the following code after watchlistMovie():

This code does something similar. But in this case, it removes the movie from the watchlist.

Now, the fragment needs to observe these changes.

Open WatchlistFragment.kt, and add the following line before onCreateView():

You'll need this import:

This is the same as you did in AllMoviesFragment earlier.

Add the following code in WatchlistFragment.kt's invalidate():

Add the imports:

This method calls the corresponding UI method based on the type of the state.

For the following changes, you will do same in AllMoviesFragment.kt too.
Now that the UI is observing changes in the state, you need to invoke the ViewModel's functions that cause the state to change.

Add the following code to addToWatchlist():

And the following code to removeFromWatchlist():

You need to observe the ViewModel's errorMessage, so add the following code to the bottom of onViewCreated():

And import the following:

Don't forget to do same in AllMoviesFragment.kt too.

Now, when the user clicks on the Add to/Remove From Watchlist button for a movie, it invokes the ViewModel to make the changes in the state.

Build and run. Click the Watchlist icon for any movie, then click the Watchlist icon on the toolbar. You'll see a new screen with only the watchlisted movies.

If you unselect any movie from the watchlist, the app immediately removes it from the screen. If you go back to the previous screen, you'll see that the app has updated the watchlist icon for the movie accordingly.

Movie app with completed watchlist

Congratulations! You've completed the tutorial and used MvRx to build a functioning watchlist app.

Download the final project using the Download Materials button at the top or bottom of this tutorial.

You've created an app that uses MvRx to manage state. It uses a shared ViewModel between multiple fragments to make communication easier. Plus, you've used the various Async types to handle asynchronous requests.

As a further enhancement, try replacing the current WatchlistRepository, which works with a mocked list of movies, to fetch films from a remote API so that the user always sees the latest movies.

Since you've used a good architecture in the app, you should be able to easily make the changes in WatchlistRepository.

You can also try adding a list of the most popular movies from the past decade. Users can click on a toggle button in the toolbar, and the app will switch between the two lists. This will help you learn about managing complex states.

As a UI enhancement, try adding a new screen that displays the details of each movie. The user can click on any movie in the list, and the app will open the details screen for that movie.

You can learn more about MvRx on the MvRx Wiki. To know more about AirBnb's motivation behind developing MvRx, you can listen to this episode of the RayWenderlich podcast.

Hopefully, you've enjoyed this tutorial! If you have any questions or ideas to share, please join the forum below.

  1. If the async call is in progress and the movies property is in Loading state, it hides the RecyclerView and shows a ProgressBar.
  2. When the async call succeeds, it hides the ProgressBar and populates the RecyclerView with the movies.
  3. If it fails, it hides the ProgressBar and shows a Toast with the failure message.
  4. init {
       // 1
       setState {
         copy(movies = Loading())
       }
       // 2
       watchlistRepository.getWatchlistedMovies()
           .execute {
             copy(movies = it)
           }
     }
    
    import com.airbnb.mvrx.Loading
    
    1. To modify the state, use setState(). In this case, you’re using copy() to make a copy of the current state and change the type of the movies property to Loading to reflect that an operation is underway.
    2. Then, you start fetching the list of movies from the repository. When it finishes, use the obtained movie list to set the new state. MvRX provides execute() as a method to convert a RxJava observable to an Async type.
    fun watchlistMovie(movieId: Long): Observable<MovieModel> {
       return Observable.fromCallable {
         val movie = movies.first { movie -> movie.id == movieId }
         movie.copy(isWatchlisted = true)
       }
     }
    
    fun removeMovieFromWatchlist(movieId: Long): Observable<MovieModel> {
       return Observable.fromCallable {
         val movie = movies.first { movie -> movie.id == movieId }
         movie.copy(isWatchlisted = false)
       }
     }
    
    val errorMessage = MutableLiveData<String>()
    
    import androidx.lifecycle.MutableLiveData
    
    fun watchlistMovie(movieId: Long) {
       withState { state ->
         if (state.movies is Success) {
           val index = state.movies.invoke().indexOfFirst {
             it.id == movieId
           }
           // 1
           watchlistRepository.watchlistMovie(movieId)
               .execute {
                 // 2
                 if (it is Success) {
                   copy(
                       movies = Success(
                           state.movies.invoke().toMutableList().apply {
                             set(index, it.invoke())
                           }
                       )
                   )
                 // 3
                 } else if (it is Fail){
                   errorMessage.postValue("Failed to add movie to watchlist")
                   copy()
                 } else {
                   copy()
                 }
               }
         }
       }
     }
    
    import com.airbnb.mvrx.Success
    import com.airbnb.mvrx.Fail
    
    1. You take a movie's ID and call the repository's watchlistMovie().
    2. If the operation succeeds, it modifies the movie list to include the watchlist status of the selected movie and updates the state accordingly.
    3. If a failure occurs, the method writes the error message to the errorMessage LiveData and copies the same state as before the operation.
    fun removeMovieFromWatchlist(movieId: Long) {
      withState { state ->
        if (state.movies is Success) {
          val index = state.movies.invoke().indexOfFirst {
            it.id == movieId
          }
          watchlistRepository.removeMovieFromWatchlist(movieId)
              .execute {
                if (it is Success) {
                  copy(
                      movies = Success(
                          state.movies.invoke().toMutableList().apply {
                            set(index, it.invoke())
                          }
                      )
                  )
                } else if (it is Fail) {
                  errorMessage.postValue("Failed to remove movie from watchlist")
                  copy()
                } else {
                  copy()
                }
              }
        }
      }
    }
    
    private val watchlistViewModel: WatchlistViewModel by activityViewModel()
    
    import com.airbnb.mvrx.activityViewModel
    
    withState(watchlistViewModel) { state ->
         when (state.movies) {
           is Loading -> {
             showLoader()
           }
           is Success -> {
             val watchlistedMovies = state.movies.invoke().filter { 
               it.isWatchlisted 
             }
             showWatchlistedMovies(watchlistedMovies)
           }
           is Fail -> {
             showError()
           }
         }
       }
    
    import com.airbnb.mvrx.withState
    import com.airbnb.mvrx.Fail
    import com.airbnb.mvrx.Loading
    import com.airbnb.mvrx.Success
    
    watchlistViewModel.watchlistMovie(movieId)
    
    watchlistViewModel.removeMovieFromWatchlist(movieId)
    
    watchlistViewModel.errorMessage.observe(viewLifecycleOwner, Observer {
      Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show()
    })
    
    import androidx.lifecycle.Observer
    
  1. To modify the state, use setState(). In this case, you’re using copy() to make a copy of the current state and change the type of the movies property to Loading to reflect that an operation is underway.
  2. Then, you start fetching the list of movies from the repository. When it finishes, use the obtained movie list to set the new state. MvRX provides execute() as a method to convert a RxJava observable to an Async type.
  1. You take a movie's ID and call the repository's watchlistMovie().
  2. If the operation succeeds, it modifies the movie list to include the watchlist status of the selected movie and updates the state accordingly.
  3. If a failure occurs, the method writes the error message to the errorMessage LiveData and copies the same state as before the operation.
init {
   // 1
   setState {
     copy(movies = Loading())
   }
   // 2
   watchlistRepository.getWatchlistedMovies()
       .execute {
         copy(movies = it)
       }
 }
import com.airbnb.mvrx.Loading
fun watchlistMovie(movieId: Long): Observable<MovieModel> {
   return Observable.fromCallable {
     val movie = movies.first { movie -> movie.id == movieId }
     movie.copy(isWatchlisted = true)
   }
 }

fun removeMovieFromWatchlist(movieId: Long): Observable<MovieModel> {
   return Observable.fromCallable {
     val movie = movies.first { movie -> movie.id == movieId }
     movie.copy(isWatchlisted = false)
   }
 }
val errorMessage = MutableLiveData<String>()
import androidx.lifecycle.MutableLiveData
fun watchlistMovie(movieId: Long) {
   withState { state ->
     if (state.movies is Success) {
       val index = state.movies.invoke().indexOfFirst {
         it.id == movieId
       }
       // 1
       watchlistRepository.watchlistMovie(movieId)
           .execute {
             // 2
             if (it is Success) {
               copy(
                   movies = Success(
                       state.movies.invoke().toMutableList().apply {
                         set(index, it.invoke())
                       }
                   )
               )
             // 3
             } else if (it is Fail){
               errorMessage.postValue("Failed to add movie to watchlist")
               copy()
             } else {
               copy()
             }
           }
     }
   }
 }
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Fail
fun removeMovieFromWatchlist(movieId: Long) {
  withState { state ->
    if (state.movies is Success) {
      val index = state.movies.invoke().indexOfFirst {
        it.id == movieId
      }
      watchlistRepository.removeMovieFromWatchlist(movieId)
          .execute {
            if (it is Success) {
              copy(
                  movies = Success(
                      state.movies.invoke().toMutableList().apply {
                        set(index, it.invoke())
                      }
                  )
              )
            } else if (it is Fail) {
              errorMessage.postValue("Failed to remove movie from watchlist")
              copy()
            } else {
              copy()
            }
          }
    }
  }
}
private val watchlistViewModel: WatchlistViewModel by activityViewModel()
import com.airbnb.mvrx.activityViewModel
withState(watchlistViewModel) { state ->
     when (state.movies) {
       is Loading -> {
         showLoader()
       }
       is Success -> {
         val watchlistedMovies = state.movies.invoke().filter { 
           it.isWatchlisted 
         }
         showWatchlistedMovies(watchlistedMovies)
       }
       is Fail -> {
         showError()
       }
     }
   }
import com.airbnb.mvrx.withState
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.Loading
import com.airbnb.mvrx.Success
watchlistViewModel.watchlistMovie(movieId)
watchlistViewModel.removeMovieFromWatchlist(movieId)
watchlistViewModel.errorMessage.observe(viewLifecycleOwner, Observer {
  Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show()
})
import androidx.lifecycle.Observer