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. 8.

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

    onAddToHistory() :SearchHistoryResult OMDB api fun onSearchMovie() : SearchMovieResult
  2. 16.

    Terminology Events Results fun onRestoreFromHistory() :SearchMovieResult Activity ViewModel SEARCH fun

    onAddToHistory() :SearchHistoryResult fun onSearchMovie() : SearchMovieResult
  3. 17.

    Terminology Events ViewState fun onRestoreFromHistory() :SearchMovieResult Activity ViewModel SEARCH fun

    onAddToHistory() :SearchHistoryResult fun onSearchMovie() : SearchMovieResult Results
  4. 18.

    -> ViewState Step 1 Events fun onRestoreFromHistory() :SearchMovieResult Activity ViewModel

    SEARCH fun onAddToHistory() :SearchHistoryResult fun onSearchMovie() : SearchMovieResult Results ViewState
  5. 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
  6. 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) }
  7. 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) }
  8. 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):
  9. 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 !!!
  10. 24.

    -> ViewState Step 1 Events fun onRestoreFromHistory() :SearchMovieResult Activity ViewModel

    SEARCH fun onAddToHistory() :SearchHistoryResult fun onSearchMovie() : SearchMovieResult Results ViewState
  11. 25.

    Step 2 Events -> Events fun onRestoreFromHistory() :SearchMovieResult Activity ViewModel

    SEARCH fun onAddToHistory() :SearchHistoryResult fun onSearchMovie() : SearchMovieResult Results ViewState
  12. 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() }
  13. 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) }
  14. 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 )
  15. 35.

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

    onSearchMovie() : SearchMovieResult Events Results Step 3 Events -> Results ViewState
  16. 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”
  17. 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
  18. 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())
  19. 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())
  20. 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
  21. 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
  22. 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>
  23. 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.
  24. 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 ->
  25. 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>
  26. 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>> { }
  27. 48.

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

    onSearchMovie() : SearchMovieResult Events Results Last step ! Results -> ViewState ViewState
  28. 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>
  29. 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>
  30. 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>
  31. 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 ✅ ✅
  32. 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
  33. 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”
  34. 55.

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

    onSearchMovie() : SearchMovieResult Events Results That’s it folks! ViewState
  35. 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
  36. 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