Home Android & Kotlin Books Advanced Android App Architecture

19
MVI Debugging Written by Aldo Olivares

In the previous chapter, you learned how to implement the MVI architecture pattern by rebuilding WeWatch. In this chapter, we’ll skip the usual unit testing with JUnit and Mockito and instead you’ll learn some helpful techniques for manually testing and debugging MVI and reactive code.

Along the way, you’ll:

  • Verify the execution of your Intents.
  • Verify the flow of your architecture.
  • Use Timber to log statements in Android.
  • Verify your Observables.
  • Use RxJava’s startWith().

Getting started

Start by opening the starter project for 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.

After Android Studio finishes building the project, run the app to see it in action.

Try adding a movie by pressing the + floating action button.

Enter a title and click the search button:

Select a movie and click OK on the Snackbar that appears:

So far, the app seems to be working fine, but you need to verify that the right Intents are getting sent and that the appropriate states are being returned.

Introducing Timber

Most developers use logs to debug their apps and test their code. To create a log statement, you typically use the Log class that comes with the Android SDK.

Log.d(TAG, "msg")
//Timber
def timberVersion = "4.7.1"
implementation "com.jakewharton.timber:timber:$timberVersion"
if (BuildConfig.DEBUG) {
  Timber.plant(Timber.DebugTree())
}
Log.d(TAG, "message")
Timber.d("message")

Testing the MVI architecture

Having an MVI architecture means you have predictable states that are triggered based on Intents. In other words, you have a unidirectional and cyclical flow for your app’s data, and this makes it easier to detect errors because you’ll know the last Intent that triggered as well as the state rendered before an exception occurs. However, to detect errors with this type of architecture you first need to make sure your app’s states are flowing as expected.

private fun observeMovieDeleteIntent() = view.deleteMovieIntent()
    .doOnNext { Timber.d("Intent: delete movie") }//Add this line
    .subscribeOn(AndroidSchedulers.mainThread())
    .observeOn(Schedulers.io())
    .flatMap<Unit> { movieInteractor.deleteMovie(it) }
    .subscribe()

private fun observeMovieDisplay() = movieInteractor.getMovieList()
    .doOnNext { Timber.d("Intent: display movie") }//Add this line
    .observeOn(AndroidSchedulers.mainThread())
    .doOnSubscribe { view.render(MovieState.LoadingState) }
    .doOnNext { view.render(it) }
    .subscribe()
override fun render(state: MovieState) {
  Timber.d("State: ${state.javaClass.simpleName}")//Add this line
  when (state) {
    is MovieState.LoadingState -> renderLoadingState()
    is MovieState.DataState -> renderDataState(state)
    is MovieState.ErrorState -> renderErrorState(state)
  }
}
D/MainActivity: State: LoadingState
D/MainPresenter$observeMovieDisplay: Intent: display movie
D/MainActivity: State: DataState
private fun observeMovieDisplayIntent() = view.displayMoviesIntent()
    .doOnNext { Timber.d("Intent: display movies") }//Add this line
    .flatMap<MovieState> { movieInteractor.searchMovies(it) }
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .doOnSubscribe { view.render(MovieState.LoadingState) }
    .subscribe { view.render(it) }
override fun render(state: MovieState) {
  Timber.d("State: ${state.javaClass.simpleName}")
  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()
  }
}
D/SearchMovieActivity: State: LoadingState
D/SearchPresenter$observeMovieDisplayIntent: Intent: display movies
D/SearchMovieActivity: State: DataState
private fun observeMovieDisplay() = movieInteractor.getMovieList()//1
    .doOnNext { Timber.d("Intent: display movie") }//2
    .observeOn(AndroidSchedulers.mainThread())//3
    .doOnSubscribe { view.render(MovieState.LoadingState) }//4
    .doOnNext { view.render(it) }//5
    .subscribe()//6

fun displayMoviesIntent(): Observable<Unit>
return Observable.just(Unit)
private fun observeMovieDisplay() = view.displayMoviesIntent()//1
    .doOnNext { Timber.d("Intent: display movie") }
    .flatMap<MovieState> { movieInteractor.getMovieList() }//2
    .observeOn(AndroidSchedulers.mainThread())
    .doOnSubscribe { view.render(MovieState.LoadingState) }
    .doOnNext { view.render(it) }
    .subscribe()

D/MainActivity: State: LoadingState
D/MainPresenter$observeMovieDisplay: Intent: display movie
D/MainActivity: State: DataState

private fun observeMovieDisplay() = view.displayMoviesIntent()
    .doOnNext { Timber.d("Intent: display movies intent") }
    .flatMap<MovieState> { movieInteractor.getMovieList() }
    .startWith(MovieState.LoadingState)
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { view.render(it) }
D/MainPresenter$observeMovieDisplay: Intent: display movies intent
D/MainActivity: State: LoadingState
D/MainActivity: State: DataState
private fun observeMovieDisplayIntent() = view.displayMoviesIntent()
    .doOnNext { Timber.d("Intent: display movies") }
    .flatMap<MovieState> { movieInteractor.searchMovies(it) }
    .startWith(MovieState.LoadingState)
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe { view.render(it) }

D/SearchPresenter$observeMovieDisplayIntent: Intent: display movies
D/SearchMovieActivity: State: LoadingState
D/SearchMovieActivity: State: DataState

Key points

  • Timber is a handy library for conditional based logging that lets you print log statements only when they meet certain conditions.
  • doOnNext() modifies your Observable source to perform a certain action when it calls onNext().
  • doOnSubscribe() executes the action passed as a parameter as soon as you subscribe to the Observable.
  • startWith() makes an Observable emit a specific sequence of items before it begins emitting the items normally expected from it.

Where to go from here?

If you want to learn more about RxJava and MVI, look at the following resources:

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

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

© 2021 Razeware LLC

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

Unlock Now

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