watchOS With SwiftUI by Tutorials!

Build awesome apps for Apple Watch with SwiftUI,
the declarative and modern way.

Home Android & Kotlin Tutorials

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.

5 / 5 4 Ratings

Version

  • Kotlin 1.3, Android 4.4, Android Studio 3.5

MvRx, pronounced “Mavericks,” is an Android framework from Airbnb. It makes it easy to build simple and complex screens for your apps. It’s based on Model-View-ViewModel architecture and is a Kotlin-first and Kotlin-only framework.

In this tutorial, you’ll learn about the core concepts of MvRx, how to implement them and how they come together to build an awesome app.

You’ll do this by creating an app that displays a list of movies and lets the user add them to a watchlist. The user can then open the watchlist to see the movies they’ve added. They can also remove films from the watchlist.

Getting Started

Download the starter project by clicking the Download Materials button at the top or bottom of the tutorial.

Inside the materials, you’ll find a starter project and a final project. Open Android Studio and select the Open an existing Android Studio project option.


Open an existing Android Studio project

Select starter-project from the download materials folder.

Select the starter project from the download materials folder

After you’ve imported the starter project, build and run. You’ll see a screen like this:

starter project initial screen

Right now, the ProgressBar is indefinite. Later in the tutorial, you’ll hide it when data loads successfully.

Clicking on the menu option in the toolbar doesn’t do anything right now. Later in the tutorial, you’ll modify it to open a new page that shows your watchlisted movies.

The starter project consists of the following files:

  • MainActivity.kt only contains fragments.
  • AllMoviesFragment.kt displays all available movies.
  • WatchlistFragment.kt displays the watchlisted movies.
  • MovieModel.kt represents the properties of a movie.
  • WatchlistRepository.kt fetches a list of movies. It simulates a network call and provides a mocked list of 10 movies after a delay of three seconds.
  • MovieAdapter.kt displays and lets the user interact with the movie list.
  • WatchlistApp.kt contains objects users can use throughout the app.
  • SplashScreenActivity.kt displays the splashscreen.

Now that you’re familiar with the materials you’ll be working with throughout this tutorial, it’s time for some background on what MVRX is and how it helps you as a developer.

Why Use MvRx?

Before you dive into an entire tutorial on how to use MvRx, take a moment to consider its advantages and whether it’s right for your project.

MvRx offers:

  • State management: The state of your app is a data structure that can represent the app’s properties at any given time. For example, in a social media app, the state could contain the user’s information and the list of recent posts.

    State management refers to the task of retaining the state across various conditions and changing from one state to another.

    MvRx makes state management easy and flexible to work with.

  • Integration with Android Architecture components: MvRx is built on top of Android Architecture Components like ViewModel and Lifecycle. This makes it easy to incrementally adopt in projects where you’re already using architecture components.
  • Conceptually based on React: React is a popular web framework that helps build reactive apps. MvRx brings the concepts of React to Android so you can take advantage of its features.

So how does MvRx work? You’ll take a look at its core functionality next.

Understanding MvRX’s Core Concepts

MvRx has four main concepts:

  1. State
  2. ViewModel
  3. View
  4. Async

Here’s a deeper look at what each of these does.

State

State is an immutable Kotlin data class that contains all of the properties necessary to represent your screen. In other words, it represents the state of your app – no pun intended. It needs to be immutable so you can safely access it from different threads. The only way to modify the state is to use the copy() operator on it. Every state class should implement MvRxState.

ViewModel

MvRx uses a class called MvRxViewModel that extends Google’s ViewModel class.

The main difference is that MvRxViewModel depends on a single immutable MvRxState instance. It doesn’t rely on LiveData to notify changes. MvRxViewModel contains methods that can modify the state; the view can use only these functions to modify the state.

You need to construct MvRxViewModel with an initial instance of MvRxState. If all the values of MvRxState have default values, MvRx will create a default instance and use it.

View

The user sees and interacts with the UI called the view, which can be a fragment or an activity. A view that needs to observe the state changes must extend the MvRxView interface.

MvRxView has a method called invalidate() that the view must implement. Whenever any property of the state changes, it calls invalidate() and updates the view.

The view can access the state using MvRxViewModel as follows:

withState(viewModel) { state ->
  ...
}

Async

Async is a wrapper class that makes it easy to deal with asynchronous sources. Async allows your state’s properties to exist in different states, depending on the ViewModelState.

For example, you might make a network request that takes a few seconds to make data available to you. While it loads, you want to show a loading symbol in the UI. To implement this, you can use a variable that stores whether the data is currently loading and changes its value when the data loads.

Now, imagine that an error occurs during the request. So, you store the error data in a separate variable.

Although this approach might work, it gets complicated when there are multiple asynchronous requests and you have to find a way to handle the errors separately.

That’s where Async comes in. It uses types to handle this information more smoothly.

Async has four types:

  1. Uninitialized: Represents that there’s not a field value yet. Use this type when you want to perform an action only if the field has a value.
  2. Loading: Represents that the field’s value is loading. This is useful when you want to show a progress bar or loading icon.
  3. Success: Use when the data has successfully loaded to stop your loading symbol and carry on with the rest of the app’s operations. This type has a property called value that contains the actual data you need.
  4. Fail: Indicates that an error occurred in the request so you can show an error UI. This type has a property called error that provides an exception with the error message. Use this to perform different actions depending on the error type, or just log the exception.

Here is an example of the different types in use:

val wordsOfTheDay: Async<List<String>> = Uninitialized
// make a network call
wordsOfTheDay = Loading
// network call succeeds
wordsOfTheDay = Sucess(resultOfNetworkCall)
println(wordsOfTheDay()) // prints the words of the day

You are now going to build the app. You will start by creating the State.

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:

  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. One more thing, tell the ViewModel to start fetching the list of movies from the repository.

    Setting the First State

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

    init {
       // 1
       setState {
         copy(movies = Loading())
       }
       // 2
       watchlistRepository.getWatchlistedMovies()
           .execute {
             copy(movies = it)
           }
     }
    

    Add the corresponding import:

    import com.airbnb.mvrx.Loading
    

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

    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.

    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

    Modifying the State

    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:

    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)
       }
     }
    

    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.

    Modifying Parts of the State

    Open WatchlistViewModel.kt and make the following code changes:

    Add the following line before the init block:

    val errorMessage = MutableLiveData<String>()
    

    And add the corresponding import:

    import androidx.lifecycle.MutableLiveData
    

    Add the following function after the init block:

    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()
                 }
               }
         }
       }
     }
    

    You'll need these imports:

    import com.airbnb.mvrx.Success
    import com.airbnb.mvrx.Fail
    

    Here what's going on in the code above:

    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.

    Now, add the following code after watchlistMovie():

    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()
                }
              }
        }
      }
    }
    

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

    Now, the fragment needs to observe these changes.

    Handling Different States

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

    private val watchlistViewModel: WatchlistViewModel by activityViewModel()
    

    You'll need this import:

    import com.airbnb.mvrx.activityViewModel
    

    This is the same as you did in AllMoviesFragment earlier.

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

    withState(watchlistViewModel) { state ->
         when (state.movies) {
           is Loading -> {
             showLoader()
           }
           is Success -> {
             val watchlistedMovies = state.movies.invoke().filter { 
               it.isWatchlisted 
             }
             showWatchlistedMovies(watchlistedMovies)
           }
           is Fail -> {
             showError()
           }
         }
       }
    

    Add the imports:

    import com.airbnb.mvrx.withState
    import com.airbnb.mvrx.Fail
    import com.airbnb.mvrx.Loading
    import com.airbnb.mvrx.Success
    

    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():

    watchlistViewModel.watchlistMovie(movieId)
    

    And the following code to removeFromWatchlist():

    watchlistViewModel.removeMovieFromWatchlist(movieId)
    

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

    watchlistViewModel.errorMessage.observe(viewLifecycleOwner, Observer {
      Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show()
    })
    

    And import the following:

    import androidx.lifecycle.Observer
    

    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.

    Where to Go From Here?

    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.

More like this

Contributors

Comments