Pro Yearly is on sale from $80 to $50! »

Unidirectional State Flow patterns
 - A refactoring story

A487b8723907637cb1af973bc5957bb4?s=47 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

A487b8723907637cb1af973bc5957bb4?s=128

Kaushik Gopal

September 28, 2018
Tweet

Transcript

  1. Unidirectional State Flow patterns
 A Refactoring Story

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

    use
  3. Movie Search Demo app

  4. Search Add To History Restore from History

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

  6. Activity ViewModel SEARCH fun onAddToHistory() :SearchHistoryResult

  7. fun onRestoreFromHistory() :SearchMovieResult Activity ViewModel SEARCH

  8. fun onRestoreFromHistory() :SearchMovieResult Problem with MVVM Activity ViewModel SEARCH fun

    onAddToHistory() :SearchHistoryResult OMDB api fun onSearchMovie() : SearchMovieResult
  9. DashboardActivity DashboardViewModel Problem with MVVM

  10. DashboardActivity DashboardViewModel Problem with MVVM

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

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

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

    onSearchMovie() : SearchMovieResult Unidirectional State Flow
  14. Terminology

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

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

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

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

    SEARCH fun onAddToHistory() :SearchHistoryResult fun onSearchMovie() : SearchMovieResult Results ViewState
  19. data class MovieViewState( val searchBoxText: String?, val searchedMovieTitle: String, val

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

    val searchedMovieRating: String, val searchedMoviePoster: String, val adapterList: List<Movie> ) { 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) }
  21. “render” method SEARCH ViewModel Activity fun render(events: MovieEvent): Observable<MovieViewState> {

    vs -> // view state searchResultTitleText.text = vs.searchedMovieTitle vs.searchBoxText?.let { searchBoxEditText.setText(it) }
  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<MovieViewState> fun render(events: MovieEvent):
  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 !!!
  24. -> ViewState Step 1 Events fun onRestoreFromHistory() :SearchMovieResult Activity ViewModel

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

    SEARCH fun onAddToHistory() :SearchHistoryResult fun onSearchMovie() : SearchMovieResult Results ViewState
  26. Activity ViewModel SEARCH

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

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

  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() }
  30. Activity ViewModel SEARCH Blade runner 2049 searchText.text.toString() SearchMovieEvent( )

  31. Activity SEARCH Blade runner 2049 val searchMovieEvents: Observable<SearchMovieEvent> = RxView.clicks(searchBtn)

    .map { } searchText.text.toString() SearchMovieEvent( )
  32. Activity SEARCH Blade runner 2049 val searchMovieEvents: Observable<SearchMovieEvent> = val

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

    Observable<AddToHistoryEvent> val restoreFromHistoryEvents: Observable<RestoreFromHistoryEvent> val events: Observable<out MovieEvent> = Observable.merge( searchMovieEvents, addToHistoryEvents, restoreFromHistoryEvents )
  34. Activity ViewModel SEARCH val events: Observable<out MovieEvent> = Observable.merge( searchMovieEvents,

    addToHistoryEvents, restoreFromHistoryEvents ) Events
  35. fun onRestoreFromHistory() :SearchMovieResult Activity ViewModel SEARCH fun onAddToHistory() :SearchHistoryResult fun

    onSearchMovie() : SearchMovieResult Events Results Step 3 Events -> Results ViewState
  36. val events: Observable<out MovieEvent> = Observable.merge( searchMovieEvents, addToHistoryEvents, restoreFromHistoryEvents )

  37. val events: Observable<out MovieEvent> = 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”
  38. val events: Observable<out MovieEvent> = Observable.merge( searchMovieEvents, addToHistoryEvents, restoreFromHistoryEvents )

    fun onSearchMovie( event SearchMovieEvent ): SearchMovieResult fun onAddToHistory( event AddToHistoryEvent ): SearchHistoryResult fun onRestoreF event Res ): SearchMovie
  39. val events: Observable<out MovieEvent> = 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())
  40. val events: Observable<out MovieEvent> 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())
  41. val events: Observable<out MovieEvent> 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
  42. val events: Observable<out MovieEvent> 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
  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<out MovieResult>
  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<out MovieResult> events. events. events.
  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<out MovieResult> ) Rx $ e e e Observable.merge( return events.publish { e ->
  46. MovieResult Lce< > sealed class Lce<T> { class Loading<T> :

    Lce<T>() data class Content<T>( val packet: T ) : Lce<T>() data class Error<T>( val packet: T ) : Lce<T>() } Loading Content (movie) Error Blog post on LCE: Modeling Data Loading in RxJava Google version: "Resource" class Observable<out MovieResult>
  47. Observable< Lce<<out MovieResult> > .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<out MovieEvent> ): Observable<Lce<out MovieResult>> { }
  48. fun onRestoreFromHistory() :SearchMovieResult Activity ViewModel SEARCH fun onAddToHistory() :SearchHistoryResult fun

    onSearchMovie() : SearchMovieResult Events Results Last step ! Results -> ViewState ViewState
  49. when (result) { // check LCE or type is Lce.Content

    -> { // check MovieResult type } is Lce.Loading -> { // … } is Lce.Error -> { // … } } Results ViewState Observable<Lce<out MovieResult>> results Observable<MovieViewState>
  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<Lce<out MovieResult>> results Observable<MovieViewState>
  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<Lce<out MovieResult>> results Observable<MovieViewState>
  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<Movie> ) Blade Runner 2049 Movie 1 Blade Movie 2 Movie 1 Movie 2 Movie 1 ✅ ✅
  53. when (result) { // check LCE or type is Lce.Content

    -> { // check MovieResult type } is Lce.Loading -> { // … } is Lce.Error -> { // … } } Results Observable<Lce<out MovieResult>> ViewState val movie: Movie = result.packet.movie return@when viewState.copy( searchedMovieTitle = movie.title, searchedMovieRating = movie.ratingSummary, searchedMoviePoster = movie.posterUrl, searchedMovieReference = movie ) Observable<Lce<out MovieResult>> results
  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<Lce<out MovieResult>> results Observable<MovieViewState> return This technique is also referred to as “reducing”
  55. fun onRestoreFromHistory() :SearchMovieResult Activity ViewModel SEARCH fun onAddToHistory() :SearchHistoryResult fun

    onSearchMovie() : SearchMovieResult Events Results That’s it folks! ViewState
  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
  57. ✌ fragmentedpodcast.com @kaushikgopal kaush.co We're hiring mobile devs! tech.instacart.com

  58. “Guided” Q&A

  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