Chapters

Hide chapters

Advanced Android App Architecture

First Edition · Android 9 · Kotlin 1.3 · Android Studio 3.2

Before You Begin

Section 0: 3 chapters
Show chapters Hide chapters

18. MVI Sample
Written by Aldo Olivares

In the previous chapter you learned all the theory behind the Model-View-Intent architecture pattern. You learned how MVI works and how each of its layers interact with each other.

Now you are going to use your new knowledge to rebuild the movies app using MVI. Along the way, you will also learn:

  • How to create an RxJava Observable from scratch.
  • How to create RxJava Observables from previously created objects with methods such as just().
  • How to use a PublishSubject.
  • How to use a Disposable and a CompositeDisposable.
  • How to integrate Room with RxJava.
  • How to integrate Retrofit with RxJava.

Ready? Lets get started.

Getting started

Start by opening the starter project for this chapter. The project contains the following packages:

  • data: contains the Room database components such as your DAOS and your MovieDatabase, your Models and your RetrofitClient.
  • domain: contains the MovieState class which you will use to represent the state of your App.
  • view: contains the activities and adapters.

Take some time to familiarize yourself with the code because you will be spending a lot of time with each file during this chapter.

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.

Build and Run the App to verify that everything is working properly.

Right now it is just an empty canvas, but that is about to change.

Going Reactive

One of the advantages of working with popular libraries developed by reliable sources such as Google is that they often include support for other popular libraries from the Android community such as RxJava.

For the WeWatch App you are using two main libraries for your backend: Retrofit and Room.

Room was developed by Google as the data persistence solution for their new Architecture Components suite of libraries for Jetpack. Since the developers behind Room are well aware that most Android Developers will use Room alongside RxJava they took the time to develop an extension that lets you return Observable callbacks when the user creates, reads, updates or deletes a record in the database.

On the other hand, since Retrofit is probably the most popular library to interact with web services, the developers at Square created an adapter github.com/JakeWharton/retrofit2-rxjava2-adapter to integrate Retrofit with RxJava a long time ago. However, thanks to the popularity of his adapter, when Retrofit 2.2 came out it included first party support for RxJava as well.

In both cases, you only need to add a few lines of code to your app level build.gradle file to take advantage of the reactive capabilities of RxJava.

Add the following to dependencies block of your app level build.gradle file:

implementation "com.squareup.retrofit2:adapter-rxjava2:$retrofitVersion"
implementation "androidx.room:room-rxjava2:$roomVersion"

Now that Room and Retrofit are ready to work with RxJava you need to modify your classes to return Observables.

Let’s start with Retrofit. Open the RetrofitClient.kt file under the data/net package and modify your Retrofit.Builder() call:

val retrofit = Retrofit.Builder()
    .baseUrl(TMDB_BASE_URL)
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(RxJava2CallAdapterFactory.create())
    .client(okHttpClient)
    .build()

This adds a RxJava2CallAdapterFactory which let’s you return an Observables from your service methods.

Now open the MovieApi.kt file and modify the searchMovies() method inside the same file to return an Observable:

@GET("search/movie")
fun searchMovie(@Query("api_key") api_key: String, @Query("query") q: String): Observable<MoviesResponse>

Next, return to the RetrofitClient.kt file and update the searchMovies() method there to return an Observable as well:

fun searchMovies(query: String): Observable<MoviesResponse> {
  return moviesApi.searchMovie(API_KEY, query)
}

That is all you need to do to make Retrofit return an Observable each time your user calls the searchMovies() method. It is time for Room to be reactive too!

The Room integration with RxJava allows different return types according to the operation being performed in your Database like Insert, Delete, Update or Query.

Insert operations in Room allow you three different return types:

  • Completable where onComplete() is called as soon as the insertion is successful. This is useful when you don’t need to know the ID of the new item inserted and there are no extra actions that need to be performed.
  • Single<Long> or Maybe<Long> where onSuccess() is called as soon as the insertion is successful and the Long value represents the ID of the item inserted. This is useful when you want to perform an additional operation immediately after the item is added to your database.
  • Single<List<Long>> or Maybe<List<Long> same as above, but the value emitted on onSuccess() is the list of rows inserted.

Update/Delete operations allow you two different return types:

  • Completable where onComplete() is called as soon as the update/delete is completed with no additional value returned.
  • Single<Integer> or Maybe<Integer> where onSuccess() is called as soon as the insertion/update is completed and the Integer value emitted represents the number of rows affected.

Query operations allow you three different return types:

  • Maybe where the value emitted on onSuccess() is the object returned from your database. If there are no records in your database that match your query Maybe will complete. If the object is later updated nothing will happen.
  • Single where the value emitted on onSuccess() is the object returned from your database. If there are no records in your database that match your query Single will call onError(EmptyResultSetException.class). If the object is updated nothing will happen.
  • Flowable/Observable where the value emitted on onNext() is the object returned from your database. If there are no records in your database that match your query Flowable/Observable won’t emit anything, neither onNext(), nor onError(). If the object is later updated Observable/Flowable will automatically emit the updated object on onNext(), allowing you to take the appropriate action in your App.

Note: Pay special attention to the Query operation because depending on the return type the behavior will be very different. For example, if you only need to retrieve a single value from your database and display it to your user Maybe might be the best choice. On the other hand, if you need to keep your UI updated Flowable/Observable might be the way to go since your Observable will keep emitting the new objects being added or modified in your database.

If you want to learn more about the return types of your Room database this article medium.com/androiddevelopers/understanding-migrations-with-room-f01e04b07929 from Google’s Android Developers blog might be very useful for you.

Open the MovieDao.kt file and modify the class methods like below:

//1
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(movie: Movie): Single<Long>
//2
@Query("select * from movie")
fun getAll(): Observable<List<Movie>>
//3
@Delete
fun delete(movie: Movie): Completable

Let’s walk through the above methods:

  1. insert() helps you create a new movie record in your Database and returns a Single with the id of the new record created.
  2. getAll() returns all the movies stored in your database inside an Observable. Since this is a Query operation every time a new record is inserted, you will receive a notification through onNext() and you can update your list of movies accordingly.
  3. delete() deletes the movie object passed as a parameter from your database and calls onComplete() as soon as the row has been deleted.

Now that Room and Retrofit are reactive and ready to be used it is time to create your Interactor.

Creating Interactors and State

Room and Retrofit are now returning Observable callbacks, but they are only emitting plain old objects such as Movie or Long. In the last chapter you learned that one of the main concepts of MVI is that you need to represent the State of your App such as Loading, Error or Data based on your models. You will use an Interactor as a way to interact with Retrofit and Room and to transform the responses into the corresponding State.

Open the MovieState.kt file under the data/domain package and add the following inside your MovieState class:

sealed class MovieState {
  object LoadingState : MovieState() //1
  data class DataState(val data: List<Movie>) : MovieState() //2
  data class ErrorState(val data: String) : MovieState() //3
  data class ConfirmationState(val movie: Movie) : MovieState() //4
  object FinishState : MovieState() //5
}

Here you are using Kotlin’s sealed classes to represent 5 different and mutually exclusive States of your App:

  1. LoadingState as the name implies, represents the State in which your App is loading data, either from Room or from the TMDB API using Retrofit.
  2. DataState represents the State in which your App is ready to display some data to the user, in this case a list of movies.
  3. ErrorState represents an error in your App such as a Network Error.
  4. ConfirmState is used to confirm that the user wants to save a new movie in your Room database.
  5. FinishState used when a View is done with its tasks.

Now that your MovieState class is ready, it’s time to create your Interactor.

Create a new class under the data package, name it MovieInteractor and make it implement the Interactor interface like below:

class MovieInteractor : Interactor

Make sure to implement all the missing members using Ctrl+ I.

Add the following properties:

private val retrofitClient = RetrofitClient()
private val movieDao = db.movieDao()

This creates instances of your RetrofitClient and your MovieDao classes to interact with your Room database and the TMDB API.

Now implement the overridden methods like this:

//1
override fun getMovieList(): Observable<MovieState> {
  return movieDao.getAll()
      .map<MovieState> { MovieState.DataState(it) }
      .onErrorReturn {
        MovieState.ErrorState("Error")
      }
}
//2
override fun deleteMovie(movie: Movie): Observable<Unit> = movieDao.delete(movie).toObservable()
//3
override fun searchMovies(title: String): Observable<MovieState> = retrofitClient.searchMovies(title)
    .observeOn(Schedulers.io())
    .map<MovieState> { it.results?.let { MovieState.DataState(it) } }
    .onErrorReturn { MovieState.ErrorState("Error") }
//4
override fun addMovie(movie: Movie): Observable<MovieState> = movieDao.insert(movie)
    .map<MovieState> {
      MovieState.FinishState
    }.toObservable()

Step by step:

  1. getMovieList() uses your movieDao.getAll() method to retrieve the list of movies from your database. The map() method helps you transform the list of movies to a MovieState.DataState. In case of an error you return a MovieState.ErrorState.

  2. deleteMovie() uses your movieDao.delete() method to delete a Movie object from your database and transforms the Complete callback to an Observable callback.

  3. searchMovies() uses your RetrofitClient’s searchMovies() method to retrieve a list of movies from the TMDB API that match the given title passed as a parameter. The map() operator transforms the response into a MovieState. Just like getMovieList(), you return a MovieState.ErrorState in case of a problem with the web service.

  4. addMovie() uses your movieDao.insert() method to insert a new movie in your database. As soon as the insertion is successful you return an Observable with a MovieState.FinishState.

And that’s it! You now have a MovieInteractor that transforms your callbacks into the appropriate State for your App. The next step is to create your Presenters.

Creating the Presenters

To connect the View with an Interactor you are going to need a Presenter for each of your activities.

Creating the MainPresenter

Create a new package under the root directory named presenter. Inside this package create a new Kotlin class named MainPresenter.

MainPresenter should accept a MovieInteractor as a constructor argument like below:

class MainPresenter(private val movieInteractor: MovieInteractor)

Then, add the following properties:

private lateinit var view: MainView
private val compositeDisposable = CompositeDisposable()

In order to render the MovieState, you’ll need a reference to a MainView. The CompositeDisposable is needed to dispose of your Disposabless once your MainPresenter is destroyed.

What is a Disposable?

You will be reading the word Disposable a lot over the next sections, but what is a Disposable? A Disposable is the object returned by the subscribe() method each time you subscribe to an Observable. This object represents a reference to a disposable resource with which the caller can use to stop receiving events.

To put it simply, you can use this object to tell your subscribers that they can stop receiving items even if the Observable is still emitting objects.

Next, add the following code to your MainPresenter:

//1
fun bind(view: MainView) {
  this.view = view
  compositeDisposable.add(observeMovieDeleteIntent())
  compositeDisposable.add(observeMovieDisplay())
}
//2
fun unbind() {
  if (!compositeDisposable.isDisposed) {
    compositeDisposable.dispose()
  }
}
//3
private fun observeMovieDeleteIntent() = view.deleteMovieIntent()
    .subscribeOn(AndroidSchedulers.mainThread())
    .observeOn(Schedulers.io())
    .flatMap<Unit> { movieInteractor.deleteMovie(it) }
    .subscribe()
//4
private fun observeMovieDisplay() = movieInteractor.getMovieList()
    .observeOn(AndroidSchedulers.mainThread())
    .doOnSubscribe { view.render(MovieState.LoadingState) }
    .doOnNext { view.render(it) }
    .subscribe()

Taking each commented section in turn:

  1. bind() lets you bind the corresponding MainView to this MainPresenter in your Activity’s onCreate() method. This method is also used to add your Disposable’s to your CompositeDisposable.

  2. unbind() is called when your MainView is destroyed. This method disposes of all the Disposable's added to your CompositeDisposable.

  3. observeMovieDeleteIntent() is called when the MainView sends an Intent to delete a movie. This method uses RxJava flatMap() operator to call the movieInteractor.deleteMovie() method to delete a movie from the database. As you can see, you used the observeOn() operator to specify that delete operation of your database has to be done on a worker thread rather than the UI thread.

  4. observeMovieDisplay() calls the mainInteractor.getMovieList() method to retrieve the list of saved movies from your database. When you subscribe to this Observable you will use the doOnSubscribe() method to tell the View to render the MovieState.LoadingState. As soon as there is a response with a MovieState you will call the view.render() method. Since your MainView has to render the State on the main thread you have to specify it using the observeOn() operator.

Note: You might notice that you didn’t have to specify a worker thread for the movieInteractor.getMovieList() method. This is because the getAll() method of your MovieDao returns an Observable and according to the documentation all Observable queries are done off the main thread. On the other hand for Insert, Update and Delete operations that return Single, Completable or Maybe you need to use the observeOn() operator to specify the Scheduler on which the operation will take place.

Creating the AddPresenter

Create a new class under the presenter package and name it AddPresenter. Replace everything inside with the following:

class AddPresenter(private val movieInteractor: MovieInteractor) {
  //1
  private val compositeDisposable = CompositeDisposable()
  private lateinit var view: AddView
  //2
  fun bind(view: AddView) {
    this.view = view
    compositeDisposable.add(observeAddMovieIntent())
  }
  //3
  fun unbind() {
    if (!compositeDisposable.isDisposed) {
      compositeDisposable.dispose()
    }
  }
  //
  fun observeAddMovieIntent() = view.addMovieIntent()
      .observeOn(Schedulers.io())
      .flatMap<MovieState> { movieInteractor.addMovie(it) }
      .observeOn(AndroidSchedulers.mainThread())
      .subscribe { view.render(it) }
}

The code for the AddPresenter class is very similar to the MainPresenter:

  1. You need an instance of your AddView to render the state of your App and a CompositeDisposable to dispose of your Disposables when your AddView is destroyed.
  2. bind() is used to get an instance of your AddView and to add your Disposables to your CompositeDisposable.
  3. unbind() disposes of all your active Disposables when your AddView is destroyed.
  4. observeAddMovieIntent() is called when AddView sends an Intent to save a movie. The flatMap() operator is used to call the addMovie() method of your MovieInteractor to add a record to your database. observeOn() is used to specify that addMovie() should be called on a worker thread while view.render() should be executed on the UI thread.

Creating the SearchPresenter

Create a new class under the presenter package and name it SearchPresenter. Replace everything inside with the following:

class SearchPresenter(private val movieInteractor: MovieInteractor) {
  //1
  private val compositeDisposable = CompositeDisposable()
  private lateinit var view: SearchView
  //2
  fun bind(view: SearchView) {
    this.view = view
    compositeDisposable.add(observeMovieDisplayIntent())
    compositeDisposable.add(observeAddMovieIntent())
    compositeDisposable.add(observeConfirmIntent())
  }
  //3
  fun unbind() {
    if (!compositeDisposable.isDisposed) {
      compositeDisposable.dispose()
    }
  }
  //4
  private fun observeConfirmIntent() = view.confirmIntent()
      .observeOn(Schedulers.io())
      .flatMap<MovieState> { movieInteractor.addMovie(it) }
      .observeOn(AndroidSchedulers.mainThread())
      .subscribe { view.render(it) }

  //5
  private fun observeAddMovieIntent() = view.addMovieIntent()
      .map<MovieState> { MovieState.ConfirmationState(it) }
      .subscribe { view.render(it) }

  //6
  private fun observeMovieDisplayIntent() = view.displayMoviesIntent()
      .flatMap<MovieState> { movieInteractor.searchMovies(it) }
      .subscribeOn(Schedulers.io())
      .observeOn(AndroidSchedulers.mainThread())
      .doOnSubscribe { view.render(MovieState.LoadingState) }
      .subscribe { view.render(it) }
}
  1. You need an instance of your SearchView to render the State and a CompositeDisposable.
  2. bind() attaches your SearchView instance to this class and adds all your Disposables to your CompositeDisposable.
  3. unbind() is called when SearchView is destroyed and uses your CompositeDisposable to dispose of all the Disposables added to it.
  4. observeConfirmIntent() is called when the SearchView sends a confirmation Intent. The flatMap() operator is used to call the addMovie() method of your MovieInteractor and map the result to a MovieState.
  5. observeAddMovieIntent() is called when your SearchView sends an Intent that the user wants to add a new movie to the database. The map() operator is used to transform the result into a ConfirmationState .
  6. observeMovieDisplayIntent() is called when your SearchView wants to display a list of movies from the TMDB API that match the title passed as a parameter. The flatmap() operator is used to call the searchMovies() method of your MovieInteractor and map the result to a MovieState.

Creating the Views

Now that the Presenter layer of your MVI architecture is ready, creating the Views should be a piece of cake :]

Creating the MainView

Open the MainActivity.kt file under the view/activity package. Right now this is an empty activity with a toolbar and a simple method to navigate to the AddMovieActivity once the user presses the + button, but that is about to change.

Start by making your MainActivity class implement the MainView interface like below:

class MainActivity : BaseActivity(), MainView {

The MainView has a simple contract given below:

interface MainView {
  fun render(state: MovieState)
  fun deleteMovieIntent(): Observable<Movie>
}

Make sure to implement all missing members by pressing Ctrl + I on your keyboard.

Add the following property to store a reference to your MainPresenter:

private lateinit var presenter: MainPresenter

And initialize it inside your onCreate() method:

presenter = MainPresenter(MovieInteractor())
presenter.bind(this)

The above code creates a new MainPresenter instance and uses its bind() method to send a reference to this View.

Next you’ll add some interactive controls to the list. You’ll make use of the left or right swipe gestures to delete a movie. Add the following code inside the deleteMovieIntent() method:

//1
val observable = Observable.create<Movie> { emitter ->
  //2
  val helper = ItemTouchHelper(
      object : ItemTouchHelper.SimpleCallback(
          0,
          ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT
      ) {
        //3
        override fun onMove(
            recyclerView: RecyclerView,
            viewHolder: RecyclerView.ViewHolder,
            target: RecyclerView.ViewHolder
        ): Boolean {
          return false
        }
        //4
        override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
          val position = viewHolder.getAdapterPosition()
          val movie = (moviesRecyclerView.adapter as MovieListAdapter).getMoviesAtPosition(position)
          emitter.onNext(movie)
        }
      })
  //5
  helper.attachToRecyclerView(moviesRecyclerView)
}
return observable

The above code creates a new Observable and emits the movie item that needs to be deleted every time the user swipes left or right. Step by step:

  1. You use the create() method to create a new Observable instance that emits a Movie object.
  2. Creates a new ItemTouchHelper object that can be attached to a RecyclerView to add swipe and move capabilities to each one of the items being displayed.
  3. For this app you are not implementing the onMove() method.
  4. Here you are overriding the onSwiped() method that lets you know when a certain item is being swiped left or right in your RecyclerView. In this case you use the Emitter onNext() method to send the item being deleted to your observers.
  5. Finally, you attach the ItemTouchHelper to your MoviesRecyclerView using the attachToRecyclerView() method.

Add the following code inside your render() method:

when (state) {
  is MovieState.LoadingState -> renderLoadingState()
  is MovieState.DataState -> renderDataState(state)
  is MovieState.ErrorState -> renderErrorState(state)
}

For the MainView you are handling three different States: Loading, Data and Error. Each one of them will be handled by a different function that you will implement.

You might notice that Android Studio is telling you that the methods called on render() have not been created. Lets do that right now by adding the following code:

//1
private fun renderLoadingState() {
  moviesRecyclerView.isEnabled = false
  progressBar.visibility = View.VISIBLE
}
//2
private fun renderDataState(dataState: MovieState.DataState) {
  progressBar.visibility = View.GONE
  moviesRecyclerView.apply {
    isEnabled = true
    (adapter as MovieListAdapter).setMovies(dataState.data)
  }
}
//3
private fun renderErrorState(dataState: MovieState.ErrorState) = longToast(dataState.data)
  1. renderLoadingState() will display a progressBar and hide your MoviesRecyclerView.
  2. renderDataState() will display your MoviesRecyclerView and hide the progressBar.
  3. renderErrorState() will display a toast with the error message received from your MainPresenter.

The only step left to finish implementing your MainView is to call your MainPresenter’s unbind() method when this activity is no longer going to be used. To do this override your MainActivitys onStop() method by adding the following code:

override fun onStop() {
  presenter.unbind()
  super.onStop()
}

This will tell your MainPresenter that your Disposable’s can be disposed since your Observables will no longer emit any new items.

Creating the AddView

Open the AddMovieActivity.kt file under the view/activity package.

Similar to what you did for your MainActivity, make your AddMovieActivity class implement the AddView interface and implement the missing members by pressing Ctrl + I. The AddView should already be given in the starter project, but is repeated below for clarity:

interface AddView {
    fun render(state: MovieState)
    fun addMovieIntent(): Observable<Movie>
}

Add the following property to store a reference to your AddPresenter:

private lateinit var presenter: AddPresenter

And initialize your AddPresenter inside onCreate():

presenter = AddPresenter(MovieInteractor())
presenter.bind(this)

The previous code just initializes the Presenter by creating an instance of your MovieInteractor, and binds it to this AddView.

Add this code inside render():

when (state) {
  is MovieState.FinishState -> renderFinishState()
}

For AddView you are only handling one State, the Finish State.

Implement the Finish state by adding the following method:

private fun renderFinishState() = startActivity(intentFor<MainActivity>().newTask().clearTask())

This code simply returns to the MainActivity after your new movie has been inserted into the database.

Now you only need to implement the Intent to add a new movie to the database. Start by adding the following property at the top of your class:

private val publishSubject: PublishSubject<Movie> = PublishSubject.create()

What is a PublishSubject? Well according to the documentation:

A Subject is a sort of bridge or proxy that is available in some implementations of ReactiveX that acts both as an observer and as an Observable. Because it is an observer, it can subscribe to one or more Observables, and because it is an Observable, it can pass through the items it observes by reemitting them, and it can also emit new items.

A PublishSubject in RxJava is basically the fusion of an Observable with an observer, it can subscribe to another Observable and emit items to its own subscribers. It is very useful in some circumstances where you can’t easily modify the return type of certain functions and it can also help you simplify the code to create a new Observable.

In this case, your PublishSubject will emit Movie objects that have to be inserted in your Room database.

Use your new PublishSubject by adding the following code:

fun addMovieClick(view: View) {
  if (titleEditText.text.toString().isNotBlank()) {
    publishSubject.onNext(Movie(title = titleEditText.text.toString(), releaseDate = yearEditText.text.toString()))
  } else {
    showMessage("You must enter a title")
  }
}

This method is executed when the user clicks the AddMovie button in your UI. If titleEditText is not blank it will tell your PublishSubject to emit the new Movie object on onNext() to its subscribers, otherwise it will display an error message.

Your PublishSubject is ready to emit new Movie objects when the user presses the Add Movie button. Now you only need to return your PublishSubject by adding the following code to your addMovieIntent() method:

override fun addMovieIntent() = publishSubject

Finally, just like you did for your MainView, you need to call your AddPresenter’s unbind() method inside onStop():

override fun onStop() {
  super.onStop()
  presenter.unbind()
}

Creating the SearchActivity

Open the SearchMovieActivity.kt file under the view/activity package. Make your SearchMovieActivity class implement the SearchView interface and implement all the missing members.

Add the following properties:

private lateinit var presenter: SearchPresenter
private val publishSubject: PublishSubject<Movie> = PublishSubject.create<Movie>()

This creates a SearchPresenter instance and a PublishSubject that emits Movie objects.

Add this code inside onCreate() to initialize your SearchPresenter and bind your SearchView:

presenter = SearchPresenter(MovieInteractor())
presenter.bind(this)

Now, add the following inside render():

when (state) {
  is MovieState.LoadingState -> renderLoadingState()
  is MovieState.DataState -> renderDataState(state)
  is MovieState.ErrorState -> renderErrorState(state)
  is MovieState.ConfirmationState -> renderConfirmationState(state)
  is MovieState.FinishState -> renderFinishState()
}

For the SearchView you are handling 5 different States: Loading, Data, Error, Confirm and Finish.

Implement the functionality of each of the above States by adding the following methods:

//1
private fun renderFinishState() = startActivity(intentFor<MainActivity>().newTask().clearTask())
//2
private fun renderLoadingState() {
  searchRecyclerView.isEnabled = false
  searchProgressBar.visibility = View.VISIBLE
}
//3
private fun renderConfirmationState(confirmationState: MovieState.ConfirmationState) {
  searchLayout.snack("Add ${confirmationState.movie.title} to your list?", Snackbar.LENGTH_LONG) {
    action(getString(R.string.ok)) {
      publishSubject.onNext(confirmationState.movie)
    }
  }
}
//4
private fun renderDataState(dataState: MovieState.DataState) {
  searchProgressBar.visibility = View.GONE
  searchRecyclerView.apply {
    isEnabled = true
    (adapter as SearchListAdapter).setMovies(dataState.data)
  }
}
//5
private fun renderErrorState(errorState: MovieState.ErrorState) {
  searchProgressBar.visibility = View.GONE
  longToast(errorState.data)
}
  1. renderFinishState() terminates the current activity and returns to the MainActivity.
  2. renderLoadingState() displays the progressBar and hides the searchRecyclerView.
  3. renderConfirmationState() displays a SnackBar asking the user to confirm the selected movie. If the user clicks the OK button in your Snackbar your PublishSubject emits a Movie object on onNext().
  4. renderDataState() hides the progressBar and displays the list of movies retrieved from the TMDB API in your SearchRecyclerView.
  5. renderErrorState() displays a toast to the user with an error message.

The only step remaining to complete the SearchView is to create the Intents with the Observables for your subscribers.

Add the following methods to implement all the Intents for this View:

//1
override fun displayMoviesIntent(): Observable<String> = Observable.just(intent.extras.getString("title"))

//2
override fun addMovieIntent(): Observable<Movie> = (searchRecyclerView.adapter as SearchListAdapter).getViewClickObservable()

//3
override fun confirmIntent(): Observable<Movie> = publishSubject
  1. displayMoviesIntent() creates an Observable that emits the movie title passed to this Activity using the just() method.
  2. addMovieIntent() creates an Observable that emits a Movie object each time an item is clicked in your SearchRecyclerView.
  3. confirmIntent() returns your PublishSubject.

And finally, override your Activity’s onStop() method to call your SearchPresenter’s unbind() just like you did for the previous Views:

override fun onStop() {
  super.onStop()
  presenter.unbind()
}

Your App is finally ready! Build and Run the App to see it in action:

Final thoughts

Take a closer look at all your Activities. As you can see, there is only one render() method that receives the current State from the Presenter and multiple Intents that represent the actions taken in your UI.

Also notice the unidirectional flow of information that this architecture provides by taking a look at your SearchView and the execution of displayMoviesIntent():

Having a structure like this makes it very easy to debug your App and guarantees that there is only one State at any given moment.

Also, you might remember from the previous chapter that State Reducers are an important concept of the MVI architecture. However, we didn’t have to use any State Reducers because the WeWatch App is rather simple and there were no States that relied on a previous State to be built.

Key points

  • Retrofit and Room include native RxJava support. This allows you to create Observable callbacks to which you can subscribe and react.

  • The Room integration with RxJava allows different return types according to the operation being performed in your Database.

  • Insert operations in Room allow you three different return types: Completable, Single<Long> and Single<List<Long>>.

  • Update and Delete operations in Room allow you two different return types: Completable and Single<Integer>.

  • Query operations in Room allow you three different return types: Maybe, Single and Observable.

  • Views in MVI have a single render() method that receives a state and renders it to the UI.

  • Views in MVI have multiple Intent methods that represent an intention to do something.

Where to go from here?

For didactic purposes, you created most of your Observables for this App from scratch, but there are many libraries out there that help you create Observables from almost any kind of object.

One of the most popular libraries to create Observables from Views in Android is RxBinding github.com/JakeWharton/RxBinding. This library is very useful to create RxJava bindings for Android UI widgets such as buttons or textviews.

For example, if you wanted to create an Observable from a button click with traditional RxJava code you would do it like this:

Observable.create { emitter ->
  button.setOnClickListener {
    emitter.onNext(queryEditText.text.toString())
  }
}

Instead, with RxBinding it is much simpler and easy to read:

button.clicks()

Another popular library is RxKotlin github.com/ReactiveX/RxKotlin, which adds convenient extension functions to RxJava. For example, take a look at the following code taken from the official repository:

val list = listOf("Alpha", "Beta", "Gamma", "Delta", "Epsilon")
list.toObservable() // extension function for Iterables
    .filter { it.length >= 5 }
    .subscribeBy(  // named arguments for lambda Subscribers
            onNext = { println(it) },
            onError =  { it.printStackTrace() },
            onComplete = { println("Done!") }
    )

You can instantly convert any Iterable (like a List) to an Observable using the toObservable() extension function!

Why don’t you take some time to rewrite some of the code in the App to use both libraries? It might be a fun challenge and you will learn a lot.

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.
© 2026 Kodeco Inc.