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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
MvRx Android on Autopilot: Getting Started
20 mins
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.
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.
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.
- If the async call is in progress and the
movies
property is inLoading
state, it hides theRecyclerView
and shows aProgressBar
. - When the async call succeeds, it hides the
ProgressBar
and populates theRecyclerView
with the movies. - If it fails, it hides the
ProgressBar
and shows aToast
with the failure message. - To modify the state, use
setState()
. In this case, you’re usingcopy()
to make a copy of the current state and change the type of the movies property toLoading
to reflect that an operation is underway. - 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 anAsync
type. - You take a movie's ID and call the repository's
watchlistMovie()
. - If the operation succeeds, it modifies the movie list to include the watchlist status of the selected movie and updates the state accordingly.
- If a failure occurs, the method writes the error message to the
errorMessage
LiveData and copies the same state as before the operation.
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:
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.
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:
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.
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.
- To modify the state, use
setState()
. In this case, you’re usingcopy()
to make a copy of the current state and change the type of the movies property toLoading
to reflect that an operation is underway. - 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 anAsync
type.
- You take a movie's ID and call the repository's
watchlistMovie()
. - If the operation succeeds, it modifies the movie list to include the watchlist status of the selected movie and updates the state accordingly.
- 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