Slide 1

Slide 1 text

Unidirectional State Flow patterns
 A Refactoring Story

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

Movie Search Demo app

Slide 4

Slide 4 text

Search Add To History Restore from History

Slide 5

Slide 5 text

Activity ViewModel OMDB api SEARCH fun onSearchMovie() : SearchMovieResult

Slide 6

Slide 6 text

Activity ViewModel SEARCH fun onAddToHistory() :SearchHistoryResult

Slide 7

Slide 7 text

fun onRestoreFromHistory() :SearchMovieResult Activity ViewModel SEARCH

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

DashboardActivity DashboardViewModel Problem with MVVM

Slide 10

Slide 10 text

DashboardActivity DashboardViewModel Problem with MVVM

Slide 11

Slide 11 text

1. state handling 2. debugging 3. testing

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

fun onRestoreFromHistory() :SearchMovieResult Activity ViewModel SEARCH fun onAddToHistory() :SearchHistoryResult fun onSearchMovie() : SearchMovieResult Unidirectional State Flow

Slide 14

Slide 14 text

Terminology

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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) }

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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):

Slide 23

Slide 23 text

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 !!!

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Activity ViewModel SEARCH

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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() }

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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”

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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())

Slide 40

Slide 40 text

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())

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

) 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

Slide 44

Slide 44 text

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.

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

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> { }

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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 ✅ ✅

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

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”

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

“Guided” Q&A

Slide 59

Slide 59 text

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