Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Unidirectional State Flow patterns
 - A refactoring story

Kaushik Gopal
September 28, 2018

Unidirectional State Flow patterns
 - A refactoring story

This is an attempt at coming up with a unidirectional state flow pattern for Android apps. I wanted to achieve the benefits of this pattern without necessarily introducing any new libraries (aside from Rx). How would one familiar with an MVVM model today step to a world where all data flows in a single direction?

Alternative titles:

- Baby steps from MVM to Redux/MVI
- MVI for us simpletons
- Unidirectional state flow patterns for dummies
- Why do mobile developers keep complicating their lives with more architecture patterns

Kaushik Gopal

September 28, 2018
Tweet

More Decks by Kaushik Gopal

Other Decks in Programming

Transcript

  1. Unidirectional State Flow
    patterns

    A Refactoring Story

    View Slide

  2. Unidirectional State Flow
    Pattern
    Understanding the
    fyi: plenty of
    Rx. use

    View Slide

  3. Movie Search
    Demo app

    View Slide

  4. Search Add To History Restore from History

    View Slide

  5. Activity
    ViewModel OMDB api
    SEARCH
    fun onSearchMovie()
    : SearchMovieResult

    View Slide

  6. Activity
    ViewModel
    SEARCH
    fun onAddToHistory()
    :SearchHistoryResult

    View Slide

  7. fun onRestoreFromHistory()
    :SearchMovieResult
    Activity
    ViewModel
    SEARCH

    View Slide

  8. fun onRestoreFromHistory()
    :SearchMovieResult
    Problem with MVVM
    Activity
    ViewModel
    SEARCH
    fun onAddToHistory()
    :SearchHistoryResult
    OMDB api
    fun onSearchMovie()
    : SearchMovieResult

    View Slide

  9. DashboardActivity
    DashboardViewModel

    Problem with MVVM

    View Slide

  10. DashboardActivity
    DashboardViewModel

    Problem with MVVM

    View Slide

  11. 1. state handling
    2. debugging
    3. testing

    View Slide

  12. Activity
    ViewModel
    SEARCH

    fun onRestoreFromHistory()
    :SearchMovieResult
    fun onAddToHistory()
    :SearchHistoryResult
    fun onSearchMovie()
    : SearchMovieResult
    Problem with MVVM

    View Slide

  13. fun onRestoreFromHistory()
    :SearchMovieResult
    Activity
    ViewModel
    SEARCH
    fun onAddToHistory()
    :SearchHistoryResult
    fun onSearchMovie()
    : SearchMovieResult

    Unidirectional State Flow

    View Slide

  14. Terminology

    View Slide

  15. Terminology
    Events
    fun onRestoreFromHistory()
    :SearchMovieResult
    Activity
    ViewModel
    SEARCH
    fun onAddToHistory()
    :SearchHistoryResult
    fun onSearchMovie()
    : SearchMovieResult

    View Slide

  16. Terminology
    Events
    Results
    fun onRestoreFromHistory()
    :SearchMovieResult
    Activity
    ViewModel
    SEARCH
    fun onAddToHistory()
    :SearchHistoryResult
    fun onSearchMovie()
    : SearchMovieResult

    View Slide

  17. Terminology
    Events
    ViewState
    fun onRestoreFromHistory()
    :SearchMovieResult
    Activity
    ViewModel
    SEARCH
    fun onAddToHistory()
    :SearchHistoryResult
    fun onSearchMovie()
    : SearchMovieResult
    Results

    View Slide

  18. -> ViewState
    Step 1
    Events
    fun onRestoreFromHistory()
    :SearchMovieResult
    Activity
    ViewModel
    SEARCH
    fun onAddToHistory()
    :SearchHistoryResult
    fun onSearchMovie()
    : SearchMovieResult
    Results
    ViewState

    View Slide

  19. data class MovieViewState(
    val searchBoxText: String?,
    val searchedMovieTitle: String,
    val searchedMovieRating: String,
    val searchedMoviePoster: String,
    val adapterList: List
    )
    SEARCH
    What does the ViewState class look like

    View Slide

  20. SEARCH
    data class MovieViewState(
    val searchBoxText: String?,
    val searchedMovieTitle: String,
    val searchedMovieRating: String,
    val searchedMoviePoster: String,
    val adapterList: List
    )
    { vs -> // view state
    }
    searchResultTitleText.text = vs.searchedMovieTitle
    searchResultRatingText.text = vs.searchedMovieRatin
    vs.searchedMoviePoster.let {
    Glide.with(ctx)
    .load(vs.searchResultImageView)
    .placeholder(spinner)
    .into(ms_mainScreen_poster)
    } ?: run {
    searchResultImageView.setImageResource(0)
    }
    listAdapter.submitList(vs.adapterList)}
    Binding ViewState to Views
    (inside Activity)
    vs.searchBoxText?.let {
    searchBoxEditText.setText(it)
    }

    View Slide

  21. “render” method
    SEARCH
    ViewModel
    Activity
    fun render(events: MovieEvent):
    Observable
    { vs -> // view state
    searchResultTitleText.text = vs.searchedMovieTitle
    vs.searchBoxText?.let {
    searchBoxEditText.setText(it)
    }

    View Slide

  22. Activity (onResume)
    -> ViewState
    vs.searchedMoviePoster.let {
    Glide.with(ctx)
    .load(vs.searchedMoviePoster)
    .placeholder(spinner)
    .into(ms_mainScreen_poster)
    } ?: run {
    ms_mainScreen_poster.setImageResource(0)
    }
    { vs -> // view state
    searchResultTitleText.text = vs.searchedMovieTitle
    searchResultRatingText.text = vs.searchedMovieRating
    vs.searchBoxText?.let {
    searchBoxEditText.setText(it)
    }
    viewModel.render(
    searchMovieEvents,
    addToHistoryEvents,
    restoreFromHistoryEvents
    )
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    Observable
    fun render(events: MovieEvent):

    View Slide

  23. viewModel.render(
    searchMovieEvents,
    addToHistoryEvents,
    restoreFromHistoryEvents
    )
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    Activity (onResume)
    vs.searchedMoviePoster.let {
    Glide.with(ctx)
    .load(vs.searchedMoviePoster)
    .placeholder(spinner)
    .into(ms_mainScreen_poster)
    } ?: run {
    ms_mainScreen_poster.setImageResource(0)
    }
    { vs -> // view state
    searchResultTitleText.text = vs.searchedMovieTitle
    searchResultRatingText.text = vs.searchedMovieRating
    vs.searchBoxText?.let {
    searchBoxEditText.setText(it)
    }
    Simpler Testing !!!

    View Slide

  24. -> ViewState
    Step 1
    Events
    fun onRestoreFromHistory()
    :SearchMovieResult
    Activity
    ViewModel
    SEARCH
    fun onAddToHistory()
    :SearchHistoryResult
    fun onSearchMovie()
    : SearchMovieResult
    Results
    ViewState

    View Slide

  25. Step 2
    Events ->
    Events
    fun onRestoreFromHistory()
    :SearchMovieResult
    Activity
    ViewModel
    SEARCH
    fun onAddToHistory()
    :SearchHistoryResult
    fun onSearchMovie()
    : SearchMovieResult
    Results
    ViewState

    View Slide

  26. Activity
    ViewModel
    SEARCH

    View Slide

  27. Activity
    ViewModel
    SEARCH
    Blade runner 2049
    searchText.text.toString()
    From Android EditText View

    View Slide

  28. Activity
    ViewModel
    SEARCH
    Blade runner 2049
    searchText.text.toString()
    SearchMovieEvent(
    )

    View Slide

  29. Activity
    ViewModel
    SEARCH
    Blade runner 2049
    searchText.text.toString()
    SearchMovieEvent(
    )
    sealed class MovieEvent {
    data class SearchMovieEvent(
    val searchedMovieTitle: String
    ) : MovieEvent()
    object AddToHistoryEvent : MovieEvent()

    data class RestoreFromHistoryEvent(
    val movieFromHistory: Movie
    ) : MovieEvent()
    }

    View Slide

  30. Activity
    ViewModel
    SEARCH
    Blade runner 2049
    searchText.text.toString()
    SearchMovieEvent(
    )

    View Slide

  31. Activity
    SEARCH
    Blade runner 2049
    val searchMovieEvents:
    Observable =
    RxView.clicks(searchBtn)
    .map {
    }
    searchText.text.toString()
    SearchMovieEvent(
    )

    View Slide

  32. Activity
    SEARCH
    Blade runner 2049
    val searchMovieEvents:
    Observable =
    val addToHistoryEvents:
    Observable =
    RxView.clicks(searchResultImageView)
    .map { AddToHistoryEvent }
    val restoreFromHistoryEvents:
    Observable =
    historyItemClickSubject
    .map { RestoreFromHistoryEvent(it) }

    View Slide

  33. Activity
    SEARCH
    Blade runner 2049
    val searchMovieEvents:
    Observable
    val addToHistoryEvents:
    Observable
    val restoreFromHistoryEvents:
    Observable
    val events: Observable =
    Observable.merge(
    searchMovieEvents,
    addToHistoryEvents,
    restoreFromHistoryEvents
    )

    View Slide

  34. Activity
    ViewModel
    SEARCH val events: Observable =
    Observable.merge(
    searchMovieEvents,
    addToHistoryEvents,
    restoreFromHistoryEvents
    )
    Events

    View Slide

  35. fun onRestoreFromHistory()
    :SearchMovieResult
    Activity
    ViewModel
    SEARCH
    fun onAddToHistory()
    :SearchHistoryResult
    fun onSearchMovie()
    : SearchMovieResult
    Events
    Results
    Step 3
    Events -> Results
    ViewState

    View Slide

  36. val events: Observable =
    Observable.merge(
    searchMovieEvents,
    addToHistoryEvents,
    restoreFromHistoryEvents
    )

    View Slide

  37. val events: Observable =
    Observable.merge(
    searchMovieEvents,
    addToHistoryEvents,
    restoreFromHistoryEvents
    )
    fun onSearchMovie(
    event SearchMovieEvent
    ): SearchMovieResult
    fun onAddToHistory(
    event AddToHistoryEvent
    ): SearchHistoryResult
    fun onRestoreF
    event Res
    ): SearchMovie
    a.k.a “Use cases”

    View Slide

  38. val events: Observable =
    Observable.merge(
    searchMovieEvents,
    addToHistoryEvents,
    restoreFromHistoryEvents
    )
    fun onSearchMovie(
    event SearchMovieEvent
    ): SearchMovieResult
    fun onAddToHistory(
    event AddToHistoryEvent
    ): SearchHistoryResult
    fun onRestoreF
    event Res
    ): SearchMovie

    View Slide

  39. val events: Observable =
    Observable.merge(
    searchMovieEvents,
    addToHistoryEvents,
    restoreFromHistoryEvents
    )
    fun onSearchMovie(
    event SearchMovieEvent
    ): SearchMovieResult
    fun onAddToHistory(
    event AddToHistoryEvent
    ): SearchHistoryResult
    fun onRestoreF
    event Res
    ): SearchMovie
    events.ofType(SearchMovieEvent::class.java).compose(onSearchMovie())

    View Slide

  40. val events: Observable
    fun onSearchMovie(
    event SearchMovieEvent
    ): SearchMovieResult
    fun onAddToHistory(
    event AddToHistoryEvent
    ): SearchHistoryResult
    fun onRestoreF
    event Res
    ): SearchMovie
    events.ofType(SearchMovieEvent::class.java).compose(onSearchMovie())
    events.ofType(AddToHistoryEvent::class.java).compose(onSearchMovie())

    View Slide

  41. val events: Observable
    fun onSearchMovie(
    event SearchMovieEvent
    ): SearchMovieResult
    fun onAddToHistory(
    event AddToHistoryEvent
    ): SearchHistoryResult
    fun onRestoreF
    event Res
    ): SearchMovie
    events.ofType(SearchMovieEvent::class.java).compose(onSearchMovie())
    events.ofType(AddToHistoryEvent::class.java).compose(onSearchMovie())
    events.ofType(RestoreFromHistoryEvent::class.java).compose(onRestoreMovie

    View Slide

  42. val events: Observable
    fun onSearchMovie(
    event SearchMovieEvent
    ): SearchMovieResult
    fun onAddToHistory(
    event AddToHistoryEvent
    ): SearchHistoryResult
    fun onRestoreF
    event Res
    ): SearchMovie
    events.ofType(SearchMovieEvent::class.java).compose(onSearchMovi
    events.ofType(AddToHistoryEvent::class.java).compose(onSearchMov
    events.ofType(RestoreFromHistoryEvent::class.java).compose(onRes
    return Observable.merge(
    )
    ): SearchMovieResult ): SearchHistoryResult ): SearchMovie

    View Slide

  43. )
    fun onSearchMovie(
    event SearchMovieEvent
    ): SearchMovieResult
    fun onAddToHistory(
    event AddToHistoryEvent
    ): SearchHistoryResult
    fun onRestoreF
    event Res
    ): SearchMovie
    events.ofType(SearchMovieEvent::class.java).compose(onSearchMovi
    events.ofType(AddToHistoryEvent::class.java).compose(onSearchMov
    events.ofType(RestoreFromHistoryEvent::class.java).compose(onRes
    return Observable.merge(
    )
    ): SearchMovieResult ): SearchHistoryResult ): SearchMovie
    Observable

    View Slide

  44. fun onSearchMovie(
    event SearchMovieEvent
    ): SearchMovieResult
    fun onAddToHistory(
    event AddToHistoryEvent
    ): SearchHistoryResult
    fun onRestoreF
    event Res
    ): SearchMovie
    .ofType(SearchMovieEvent::class.java).compose(onSearchMovi
    .ofType(AddToHistoryEvent::class.java).compose(onSearchMov
    .ofType(RestoreFromHistoryEvent::class.java).compose(onRes
    return Observable.merge(
    )
    ): SearchMovieResult ): SearchHistoryResult ): SearchMovie
    sealed class MovieResult {
    data class SearchMovieResult(
    val movie: Movie
    ) : MovieResult()
    data class SearchHistoryResult
    val movieHistory: Movie
    ) : MovieResult()
    }
    Observable
    events.
    events.
    events.

    View Slide

  45. fun onSearchMovie(
    event SearchMovieEvent
    ): SearchMovieResult
    fun onAddToHistory(
    event AddToHistoryEvent
    ): SearchHistoryResult
    fun onRestoreF
    event Res
    ): SearchMovie
    .ofType(SearchMovieEvent::class.java).compose(onSearchMovi
    .ofType(AddToHistoryEvent::class.java).compose(onSearchMov
    .ofType(RestoreFromHistoryEvent::class.java).compose(onRes
    ): SearchMovieResult ): SearchHistoryResult ): SearchMovie
    sealed class MovieResult {
    data class SearchMovieResult(
    val movie: Movie
    ) : MovieResult()
    data class SearchHistoryResult
    val movieHistory: Movie
    ) : MovieResult()
    }
    Observable
    )
    Rx $
    e
    e
    e
    Observable.merge(
    return events.publish { e ->

    View Slide

  46. MovieResult
    Lce< >
    sealed class Lce {
    class Loading : Lce()
    data class Content(
    val packet: T
    ) : Lce()
    data class Error(
    val packet: T
    ) : Lce()
    }
    Loading
    Content
    (movie)
    Error
    Blog post on LCE: Modeling Data Loading in RxJava
    Google version: "Resource" class
    Observable

    View Slide

  47. Observable< Lce< >
    .ofType(SearchMovieEvent::class.java).compose(onSea
    .ofType(AddToHistoryEvent::class.java).compose(onSe
    .ofType(RestoreFromHistoryEvent::class.java).compos
    )
    e
    e
    e
    Observable.merge(
    return events.publish { e ->
    private fun eventsToResults(
    events: Observable
    ): Observable> {
    }

    View Slide

  48. fun onRestoreFromHistory()
    :SearchMovieResult
    Activity
    ViewModel
    SEARCH
    fun onAddToHistory()
    :SearchHistoryResult
    fun onSearchMovie()
    : SearchMovieResult
    Events
    Results
    Last step !
    Results -> ViewState
    ViewState

    View Slide

  49. when (result) { // check LCE or type
    is Lce.Content -> { // check MovieResult type
    }
    is Lce.Loading -> { // … }
    is Lce.Error -> { // … }
    }
    Results
    ViewState
    Observable> results
    Observable

    View Slide

  50. when (result) { // check LCE or type
    is Lce.Content -> { // check MovieResult type
    }
    is Lce.Loading -> { // … }
    is Lce.Error -> { // … }
    }
    Results
    ViewState
    val movie: Movie = result.packet.movie
    return@when viewState.copy(
    searchedMovieTitle = movie.title,
    searchedMovieRating = movie.ratingSummary,
    searchedMoviePoster = movie.posterUrl
    )
    Observable> results
    Observable

    View Slide

  51. when (result) { // check LCE or type
    is Lce.Content -> { // check MovieResult type
    // …
    }
    is Lce.Loading -> { // … }
    is Lce.Error -> { // … }
    }
    Results
    ViewState
    We want to accumulate note overwrite!
    Observable> results
    Observable

    View Slide

  52. We want to accumulate note overwrite!
    data class MovieViewState(
    val searchBoxText: String,
    val searchedMovieTitle: String,
    val searchedMovieRating: String,
    val searchedMoviePoster: String,
    val adapterList: List
    )
    Blade Runner 2049
    Movie 1
    Blade
    Movie 2
    Movie 1
    Movie 2
    Movie 1


    View Slide

  53. when (result) { // check LCE or type
    is Lce.Content -> { // check MovieResult type
    }
    is Lce.Loading -> { // … }
    is Lce.Error -> { // … }
    }
    Results
    Observable> ViewState
    val movie: Movie = result.packet.movie
    return@when viewState.copy(
    searchedMovieTitle = movie.title,
    searchedMovieRating = movie.ratingSummary,
    searchedMoviePoster = movie.posterUrl,
    searchedMovieReference = movie
    )
    Observable> results

    View Slide

  54. Results
    ViewState
    when (result) { // check LCE or type
    is Lce.Content -> { // check MovieResult type
    // …
    }
    is Lce.Loading -> { // … }
    is Lce.Error -> { // … }
    }
    results
    .scan(startingViewState) { accumulatedState, result ->
    Rx $
    Observable> results
    Observable
    return
    This technique is also referred to as “reducing”

    View Slide

  55. fun onRestoreFromHistory()
    :SearchMovieResult
    Activity
    ViewModel
    SEARCH
    fun onAddToHistory()
    :SearchHistoryResult
    fun onSearchMovie()
    : SearchMovieResult
    Events
    Results
    That’s it folks!
    ViewState

    View Slide

  56. Resources
    Blog post on LCE: Modeling Data Loading in RxJava
    Google version: "Resource" class
    LCE modeling
    The State of Managing State with RxJava
    Unidirectional state
    https://jakewharton.com/the-state-of-managing-state-with-rxjava/
    MVI patterns with Hannes Dorfmann
    http://hannesdorfmann.com/android/mosby3-mvi-1
    Sample app
    https://github.com/kaushikgopal/movies-usf
    Movie Review Sample app
    https://tech.instacart.com/lce-modeling-data-loading-in-rxjava-b798ac98d80
    https://github.com/googlesamples/android-architecture-components/blob/master/GithubBrowserSamp
    Rx by example – Volume 3 (the multicast edition)
    Rx sharing

    View Slide


  57. fragmentedpodcast.com
    @kaushikgopal
    kaush.co
    We're hiring mobile devs!
    tech.instacart.com

    View Slide

  58. “Guided” Q&A

    View Slide

  59. How does this make
    testing easier?
    What about
    savedInstanceState? Can
    we also handle this?
    How does this make
    debugging easier?
    What do you think
    about routing and navigation?
    What about activity
    rotation for Android apps?
    1
    2
    6
    7
    3 Can you show me your
    tests? Are they meaningful?
    8
    Do I “need” Rx for this?
    4 What are some of your
    favorite English movies?
    9
    With repeated ViewState
    rendering, 

    is performance a problem?
    5

    View Slide