Slide 1

Slide 1 text

Why MVI? “Curious case of yet another pattern" Garima Jain @ragdroid

Slide 2

Slide 2 text

@ragdroid @droidconBos Why MVI? MVP / MVVM -> MVI

Slide 3

Slide 3 text

@ragdroid @droidconBos Part 1 Why MVI?

Slide 4

Slide 4 text

@ragdroid @droidconBos MVI

Slide 5

Slide 5 text

@ragdroid @droidconBos Model View Intent

Slide 6

Slide 6 text

@ragdroid @droidconBos State View Intention Model View Intent

Slide 7

Slide 7 text

@ragdroid @droidconBos User Intent State View State View Intention

Slide 8

Slide 8 text

@ragdroid @droidconBos User Intent State View Interacts Changes Updates Seen State View Intention

Slide 9

Slide 9 text

@ragdroid @droidconBos Model View Intent State

Slide 10

Slide 10 text

@ragdroid @droidconBos State Single Source of Truth

Slide 11

Slide 11 text

@ragdroid @droidconBos printing length completed noInternet loggedIn size scanning index State sPullToRefreshing List isButtonEnabled pageNo electedItem isChecked e r r o r M s g i s V i s i B l e i s L o a d i n g sLoadMore pen r u n n i n g active l i k e d l o c a t i o n i d l e

Slide 12

Slide 12 text

@ragdroid @droidconBos State

Slide 13

Slide 13 text

@ragdroid @droidconBos ItemsFragment State

Slide 14

Slide 14 text

@ragdroid @droidconBos State { loading = true items = EMPTY } State

Slide 15

Slide 15 text

@ragdroid @droidconBos State { loading = false items = EMPTY emptyStateVisible = true } State

Slide 16

Slide 16 text

@ragdroid @droidconBos State { loading = false refreshing = true items = EMPTY emptyStateVisible = true } State

Slide 17

Slide 17 text

@ragdroid @droidconBos State { loading = false refreshing = false items = EMPTY emptyStateVisible = false errorMsg = “No Internet” } State

Slide 18

Slide 18 text

@ragdroid @droidconBos State { loading = false refreshing = false items = List emptyStateVisible = false errorMsg = “” } State

Slide 19

Slide 19 text

@ragdroid @droidconBos State Problem

Slide 20

Slide 20 text

@ragdroid @droidconBos

Slide 21

Slide 21 text

@ragdroid @droidconBos View Presenter Data User Time

Slide 22

Slide 22 text

@ragdroid @droidconBos View Presenter showLoading( ) Result : [ ] Data User start() refresh() loadItems( ) setRefreshing( ) Result : [ ] showEmptyState( ) hideLoading( ) showEmptyState( ) hideLoading( )?? refresh( )

Slide 23

Slide 23 text

@ragdroid @droidconBos Presenter loadItems( ) refresh( ) Multiple Inputs

Slide 24

Slide 24 text

@ragdroid @droidconBos Presenter showEmptyState( ) hideLoading( ) showEmptyState( ) hideLoading( )?? Multiple Outputs !!!

Slide 25

Slide 25 text

@ragdroid @droidconBos State of UI coordinated by Presenter (or VM) Presenter, Business Logic and View : own states. MVI: Single Source of Truth : Hannes Model / View State MVI: Presenter has a single output : f(x) = y OR f(a,b,c) = y State Problem

Slide 26

Slide 26 text

@ragdroid @droidconBos MVP / MVVM -> MVI Part 2

Slide 27

Slide 27 text

@ragdroid @droidconBos MVPI

Slide 28

Slide 28 text

@ragdroid @droidconBos View Presenter Data State { loading = true refreshing = false items = EMPTY emptyStateVisible = false errorMsg = “” } UIEvent Reducer newState Action initialState Result MVPI with Initial State

Slide 29

Slide 29 text

@ragdroid @droidconBos View Presenter Data State { loading = true refreshing = false items = EMPTY emptyStateVisible = false errorMsg = “” } UIEvent Reducer newState Action Result oldState MVPI with Old State

Slide 30

Slide 30 text

@ragdroid @droidconBos View Presenter Data Start Reducer newState LoadItems Loading Loading initialState State { loading = true refreshing = false items = EMPTY emptyStateVisible = false errorMsg = “” }

Slide 31

Slide 31 text

@ragdroid @droidconBos View Presenter Data State { loading = true refreshing = false items = EMPTY emptyStateVisible = false errorMsg = “” } Start Reducer newState Loading initialState Loading LoadItems

Slide 32

Slide 32 text

@ragdroid @droidconBos View Presenter Data Reducer Empty Start Result : [ ] Empty oldState newState State { loading = true refreshing = false items = EMPTY emptyStateVisible = false errorMsg = “” } LoadItems

Slide 33

Slide 33 text

@ragdroid @droidconBos View Presenter Data State { loading = false refreshing = false items = EMPTY emptyStateVisible = true errorMsg = “” } Start Result : [ ] Reducer newState oldState Empty Empty LoadItems

Slide 34

Slide 34 text

@ragdroid @droidconBos View Presenter Data Pull To Refresh Reducer newState RefreshItems oldState Refreshing Refreshing State { loading = false refreshing = false items = EMPTY emptyStateVisible = true errorMsg = “” }

Slide 35

Slide 35 text

@ragdroid @droidconBos View Presenter Data Pull To Refresh Reducer newState RefreshItems oldState Refreshing Refreshing State { loading = false refreshing = true items = EMPTY emptyStateVisible = true errorMsg = “” }

Slide 36

Slide 36 text

@ragdroid @droidconBos View Presenter Data Pull To Refresh Reducer newState RefreshItems Result : [ ] oldState RefreshEmpty RefreshEmpty State { loading = false refreshing = true items = EMPTY emptyStateVisible = true errorMsg = “” }

Slide 37

Slide 37 text

@ragdroid @droidconBos View Presenter Data State { loading = false refreshing = false items = EMPTY emptyStateVisible = true errorMsg = “” } Pull To Refresh Reducer newState RefreshItems Result : [ ] oldState RefreshEmpty RefreshEmpty

Slide 38

Slide 38 text

@ragdroid @droidconBos View Presenter Data State { loading = true refreshing = false items = EMPTY emptyStateVisible = false errorMsg = “” } UIEvent Reducer newState Action Result oldState MVPI with Concurrent Events UIEvent Action Result oldState newState

Slide 39

Slide 39 text

@ragdroid @droidconBos Rules of Thumb

Slide 40

Slide 40 text

@ragdroid @droidconBos Plain Objects Must have a type which is not undefined Convey intention of user User triggers Actions Action

Slide 41

Slide 41 text

@ragdroid @droidconBos Immutable : List / Object nothing is mutable Set representing the state of View Any change is triggered only by dispatching an action State

Slide 42

Slide 42 text

@ragdroid @droidconBos Function of previous state & result (action/ partialState) f(previousState, Result) = new State Return current state for unknown result Reducer

Slide 43

Slide 43 text

@ragdroid @droidconBos Code

Slide 44

Slide 44 text

@ragdroid @droidconBos interface View { /** * Expose various intents */ fun pullToRefreshIntent(): Observable fun loadingIntent(): Observable fun render(state: State) } View

Slide 45

Slide 45 text

@ragdroid @droidconBos fun pullToRefreshIntent(): Observable = refreshLayout.refreshes() .map { ignored -> true } View fun loadingIntent(): Observable = Observable.just(true)

Slide 46

Slide 46 text

@ragdroid @droidconBos data class State( val loading: Boolean, val items: List, val loadingError: Throwable?, val pullToRefreshing: Boolean, val pullToRefreshError: Throwable?, val emptyStateVisible: Boolean) State

Slide 47

Slide 47 text

@ragdroid @droidconBos sealed class Result { object Loading: Result() object LoadingEmpty: Result() data class LoadingError(val throwable: Throwable): Result() data class LoadingComplete(val characters: List): Result() object PullToRefreshing: Result() object PullToRefreshEmpty: Result() data class PullToRefreshError(val throwable: Throwable): Result() data class PullToRefreshComplete(val characters: List): Result() } Result

Slide 48

Slide 48 text

@ragdroid @droidconBos fun attachView(view: View) { val loadingResult = subscribeToLoading() val pullToRefreshResult = subscribeToPullToRefresh() val allResultObservable = Observable.merge(loadingResult, pullToRefreshResult) subscribeToStates(allResultObservable) } Presenter

Slide 49

Slide 49 text

@ragdroid @droidconBos interface Repository { fun loadItems(): Observable> } Repository

Slide 50

Slide 50 text

@ragdroid @droidconBos fun subscribeToLoading(): Observable = view.loadingIntent() .flatMap { repository.loadItems() .map { items -> if (items.isEmpty()) Result.LoadingEmpty else Result.LoadingComplete(items) } } .onErrorReturn { Result.LoadingError(it) } Presenter

Slide 51

Slide 51 text

@ragdroid @droidconBos fun subscribeToPullToRefresh(): Observable { return view.pullToRefreshIntent() .flatMap { repository.loadItems() .map { items -> if (items.isEmpty()) Result.PullToRefreshEmpty else Result.PullToRefreshComplete(items) } .startWith(Result.PullToRefreshing) } .onErrorReturn { Result.PullToRefreshError(it) } } Presenter

Slide 52

Slide 52 text

@ragdroid @droidconBos fun subscribeToStates(allResultObservable): Disposable { return allResultObservable .scan(State.init()) { previousState, result -> reducer(previousState, result) } .observeOn(UISched) .subscribe( { state -> view.render(state) }, { Timber.e(it) } ) } Presenter

Slide 53

Slide 53 text

@ragdroid @droidconBos fun reducer(previousState: State, result: Result): State = when (result) { is Result.Loading -> previousState.copy( loading = true, loadingError = null) is Result.LoadingError -> previousState.copy( loading = false, loadingError = result.throwable) is Result.LoadingComplete -> previousState.copy( loading = false, loadingError = null, items = result.characters) is Result.LoadingEmpty -> previousState.copy( loading = false, loadingError = null, items = emptyList(), emptyStateVisible = true) ... } Presenter

Slide 54

Slide 54 text

@ragdroid @droidconBos fun reducer(previousState: State, result: Result): State = when (result) { ... is Result.PullToRefreshing -> previousState.copy( loading = false, pullToRefreshing = true, pullToRefreshError = null) is Result.PullToRefreshError -> previousState.copy( pullToRefreshing = false, pullToRefreshError = result.throwable) is Result.PullToRefreshComplete -> previousState.copy( pullToRefreshing = false, pullToRefreshError = null, items = result.characters) is Result.PullToRefreshEmpty -> previousState.copy( pullToRefreshing = false, loadingError = null, items = emptyList(), emptyStateVisible = true ) } Presenter

Slide 55

Slide 55 text

@ragdroid @droidconBos fun render(state: State) { binding.model = state when { state.pullToRefreshError != null -> return@render state.emptyStateVisible -> return@render state.loadingError != null -> { //show error return@render } else -> { //show list } } } View

Slide 56

Slide 56 text

@ragdroid @droidconBos Why MVI? Hannes Dorfmann MVI Series

Slide 57

Slide 57 text

@ragdroid @droidconBos State Problem

Slide 58

Slide 58 text

@ragdroid @droidconBos Orientation Changes

Slide 59

Slide 59 text

@ragdroid @droidconBos Navigation & Pop Backstack

Slide 60

Slide 60 text

@ragdroid @droidconBos Process Death

Slide 61

Slide 61 text

@ragdroid @droidconBos Immutability

Slide 62

Slide 62 text

@ragdroid @droidconBos Debuggable

Slide 63

Slide 63 text

@ragdroid @droidconBos Testability

Slide 64

Slide 64 text

@ragdroid @droidconBos Conclusion

Slide 65

Slide 65 text

@ragdroid @droidconBos If you model your system as pure functions, you will get a finite state machine - @ragdroid Try to avoid side-effects and model your screens as pure functions. - @ragdroid MVI is MVP or MVVM done right. - @_riteshhh MVI prevents us from misusing patterns like MVP or MVVM. - @_riteshhh

Slide 66

Slide 66 text

@ragdroid @droidconBos http://hannesdorfmann.com/android/mosby3-mvi-1 - Hannes Dorfmann http://fragmentedpodcast.com/tag/hannes-dorfmann/ - Hannes Dorfmann https://www.youtube.com/watch?v=0IKHxjkgop4 - Jake Wharton https://www.youtube.com/watch?v=64rQ9GKphTg - Benoit Quenaudon https://github.com/oldergod - Benoit Quenaudon https://egghead.io/courses/getting-started-with-redux - Dan Abramov References

Slide 67

Slide 67 text

@ragdroid @droidconBos https://github.com/ragdroid/klayground Demo

Slide 68

Slide 68 text

FUELED http://fueled.com/garima [email protected] THANK YOU