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

Reactive StateManagement with Model-View-Intent

Reactive StateManagement with Model-View-Intent

mobiconf 2017, Krakow

Managing application state is not a simple topic especially on Android with a synchronous and asynchronous source of data, components having different lifecycles, back stack navigation and process death.

Model-View-Intent (MVI) is an architectural design pattern to separate the View from the Model. In this talk, we will discuss the idea behind MVI and how this pattern compares to other architectural patterns like Flux, Redux, Model-View-Presenter or Model-View-ViewModel. Furthermore, we will discuss what a Model actually is and how Model is related to State.

Once we have understood the role of Model and State we will focus on state management by building an unidirectional data flow.

Finally, we will connect the dots with RxJava to build apps with deterministic state management in a reactive way that makes maintaining and testing such apps easy.

Hannes Dorfmann

October 05, 2017
Tweet

More Decks by Hannes Dorfmann

Other Decks in Programming

Transcript

  1. 1

  2. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 Rainer Weiss Kip Thorne

    Barry Barish
  3. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 Rainer Weiss Kip Thorne

    Barry Barish Nobel laureates in Physics 2017 Gravitational waves
  4. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 General relativity

  5. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017

  6. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 a thousand part of

    a proton
  7. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 2 black holes melded

    1.3 Billion years ago Gravitational waves detected
  8. 8 REACTIVE STATEMANAGEMENT WITH MODEL-VIEW-INTENT

  9. 9 HELLO I’M HANNES

  10. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 MVP MVC MVVM MVC

    MVI
  11. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 MVP MVC MVVM MVI

  12. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 What is a Model?

  13. class PersonsPresenter : Presenter<PersonsView> { fun loadPersons() { view?.showLoading(true) backend.loadPersons({

    persons : List<Person> -> view?.showPersons(persons) }, { error: Throwable -> view?.showError(error) }) } }
  14. class PersonsPresenter : Presenter<PersonsView> { fun loadPersons() { view?.showLoading(true) backend.loadPersons({

    persons : List<Person> -> view?.showPersons(persons) }, { error: Throwable -> view?.showError(error) }) } }
  15. class PersonsPresenter : Presenter<PersonsView> { fun loadPersons(){ view?.showLoading(true) backend.loadPersons({ persons

    : List<Person> -> view?.showPersons(persons) }, { error: Throwable -> view?.showError(error) }) } }
  16. class PersonsPresenter : Presenter<PersonsView> { fun loadPersons(){ view?.showLoading(true) backend.loadPersons({ persons

    : List<Person> -> view?.showPersons(persons) }, { error: Throwable -> view?.showError(error) }) } }
  17. class PersonsPresenter : Presenter<PersonsView> { fun loadPersons(){ view?.showLoading(true) backend.loadPersons({ persons

    : List<Person> -> view?.showPersons(persons) }, { error: Throwable -> view?.showError(error) }) } }
  18. class PersonsPresenter : Presenter<PersonsView> { fun loadPersons(){ view?.showLoading(true) // Displays

    a ProgressBar backend.loadPersons({ persons : List<Person> -> view?.showPersons(persons) // Displays a ProgressBar }, { error: Throwable -> view?.showError(error) // Displays a error message }) } }
  19. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 What is a Model?

  20. class PersonsViewModel : ViewModel { val loadingLiveData : MutableLiveData<Boolean> …

    fun loadPersons(){ loadingLiveData.setValue((true) backend.loadPersons({ persons : List<Person> -> loadingLiveData.setValue( false ) personsLiveData.setValue( persons ) }, { error: Throwable -> loadingLiveData.setValue( false ) errorLiveData.setValue( error ) }) } }
  21. class PersonsViewModel : ViewModel { val loadingLiveData : MutableLiveData<Boolean> …

    fun loadPersons(){ loadingLiveData.setValue((true) backend.loadPersons({ persons : List<Person> -> loadingLiveData.setValue( false ) personsLiveData.setValue( persons ) }, { error: Throwable -> loadingLiveData.setValue( false ) errorLiveData.setValue( error ) }) } }
  22. class PersonsViewModel : ViewModel { val loadingLiveData : MutableLiveData<Boolean> …

    fun loadPersons(){ loadingLiveData.setValue((true) backend.loadPersons({ persons : List<Person> -> loadingLiveData.setValue( false ) personsLiveData.setValue( persons ) }, { error: Throwable -> loadingLiveData.setValue( false ) errorLiveData.setValue( error ) }) } }
  23. class PersonsViewModel : ViewModel { val loadingLiveData : MutableLiveData<Boolean> …

    fun loadPersons(){ loadingLiveData.setValue((true) backend.loadPersons({ persons : List<Person> -> loadingLiveData.setValue( false ) personsLiveData.setValue( persons ) }, { error: Throwable -> loadingLiveData.setValue( false ) errorLiveData.setValue( error ) }) } }
  24. class PersonsViewModel : ViewModel { val loadingLiveData : MutableLiveData<Boolean> …

    fun loadPersons(){ loadingLiveData.setValue((true) backend.loadPersons({ persons : List<Person> -> loadingLiveData.setValue( false ) personsLiveData.setValue( persons ) }, { error: Throwable -> loadingLiveData.setValue( false ) errorLiveData.setValue( error ) }) } }
  25. class PersonsViewModel : ViewModel { val loadingLiveData : MutableLiveData<Boolean> …

    fun loadPersons(){ loadingLiveData.setValue((true) backend.loadPersons({ persons : List<Person> -> loadingLiveData.setValue( false ) personsLiveData.setValue( persons ) }, { error: Throwable -> loadingLiveData.setValue( false ) errorLiveData.setValue( error ) }) } }
  26. class PersonsViewModel : ViewModel { val loadingLiveData : MutableLiveData<Boolean> …

    fun loadPersons(){ loadingLiveData.setValue((true) backend.loadPersons({ persons : List<Person> -> loadingLiveData.setValue( false ) personsLiveData.setValue( persons ) }, { error: Throwable -> loadingLiveData.setValue( false ) errorLiveData.setValue( error ) }) } }
  27. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 What is a Model?

  28. data class PersonsModel ( val loading : boolean, val persons

    : List<Person>?, val error : Throwable? )
  29. class PersonsPresenter : Presenter<PersonsView> { fun loadPersons(){ view?.render(PersonsModel ( true,

    null, null) ) backend.loadPersons({ persons : List<Person> -> view?.render(PersonsModel ( false, persons, null) ) }, { error: Throwable -> view?.render(PersonsModel ( false, null, error) ) }) } }
  30. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 Model == State

  31. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 How do we change

    State?
  32. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 User

  33. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 User

  34. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 User Business Logic intent

  35. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 User Business Logic intent

    state
  36. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 User Business Logic intent

    state render(state)
  37. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 Model-View-Intent

  38. None
  39. None
  40. interface SearchView { fun searchIntent() : Observable<String> fun render(viewState :

    SearchViewState) }
  41. data class SearchViewState ( val error : Throwable? val loading:

    boolean val result : List<Product>? )
  42. class SearchFragment : Fragment , SearchView { ... override fun

    searchIntent() : Observable<String> { return RxSearchView.queryTextChanges(searchView) // Thanks Jake Wharton } }
  43. class SearchFragment : Fragment , SearchView { ... override fun

    render( viewState : SearchViewState) { if (viewState.loading) renderLoading() else if (viewState.result != null) renderResult(viewState.result) else if (viewState.error != null) renderError() else throw IllegalStateException("Unknown ViewState") } }
  44. class SearchPresenter : MviBasePresenter<SearchView> { val SearchInteractor searchInteractor; override fun

    bindIntents() { val searchState : Observable<SearchViewState> = intent(SearchView::searchIntent) .filter { term -> term.size >= 3 } .switchMap(searchInteractor::search) subscribeViewState(searchState, SearchView::render) } }
  45. class SearchInteractor { val searchEngine : SearchEngine // Makes http

    calls fun search(searchString : String) : Observable<SearchViewState>{ return searchEngine.searchFor(searchString) // Observable<List<Product>> .map(products -> SearchViewState(false, products, null)) .startWith(SearchViewState(true, null, null)) .onErrorReturn(error -> SearchViewState(false, null, error)); } }
  46. class SearchInteractor { val searchEngine : SearchEngine // Makes http

    calls fun search(searchString : String) : Observable<SearchViewState>{ return searchEngine.searchFor(searchString) // Observable<List<Product>> .map(products -> new SearchViewState(false, products, null)) .startWith(new SearchViewState(true, null, null)) .onErrorReturn(error -> new SearchViewState(false, null, error)); } }
  47. class SearchInteractor { val searchEngine : SearchEngine // Makes http

    calls fun search(searchString : String) : Observable<SearchViewState>{ return searchEngine.searchFor(searchString) // Observable<List<Product>> .map(products -> new SearchViewState(false, products, null)) .startWith(new SearchViewState(true, null, null)) .onErrorReturn(error -> new SearchViewState(false, null, error)); } }
  48. class SearchInteractor { val searchEngine : SearchEngine // Makes http

    calls fun search(searchString : String) : Observable<SearchViewState>{ return searchEngine.searchFor(searchString) // Observable<List<Product>> .map(products -> new SearchViewState(false, products, null)) .startWith(new SearchViewState(true, null, null)) .onErrorReturn(error -> new SearchViewState(false, null, error)); } }
  49. class SearchInteractor { val searchEngine : SearchEngine // Makes http

    calls fun search(searchString : String) : Observable<SearchViewState>{ return searchEngine.searchFor(searchString) // Observable<List<Product>> .map(products -> new SearchViewState(false, products, null)) .startWith(new SearchViewState(true, null, null)) .onErrorReturn(error -> new SearchViewState(false, null, error)); } }
  50. Search … searchIntent view.render( model )

  51. C searchIntent filter() C view.render( model )

  52. Ca searchIntent filter() C Ca view.render( model )

  53. Cam searchIntent Cam filter() C Ca view.render( model )

  54. Cam searchIntent filter() flatMap() view.render( model ) Cam C Ca

    Cam
  55. Cam searchIntent filter() flatMap() startWith( new Loading() ) Cam C

    Ca Cam
  56. Cam searchIntent filter() flatMap() startWith( new Loading() ) L view.render(

    SearchViewState.Loading) Cam C Ca Cam
  57. Cam searchIntent filter() flatMap() startWith( new Loading() ) L view.render(

    SearchViewState.Loading) Cam C Ca Cam
  58. Cam searchIntent filter() flatMap() startWith( new Loading() ) L view.render(

    SearchViewState.Loading) Cam C Ca Cam
  59. Cam searchIntent filter() flatMap() startWith( new Loading() ) L view.render(

    SearchViewState.Loading) Cam C Ca Cam
  60. Cam searchIntent filter() flatMap() startWith( new Loading() ) L view.render(

    SearchViewState.Loading) Cam C Ca Cam
  61. Cam searchIntent filter() flatMap() startWith( new Loading() ) L Cam

    C Ca Cam Cam
  62. Cam searchIntent filter() flatMap() startWith( new Loading() ) L Cam

    C Ca Cam Cam
  63. Cam searchIntent filter() flatMap() startWith( new Loading() ) L Cam

    C Ca Cam Cam
  64. Cam searchIntent filter() flatMap() startWith( new Loading() ) L Cam

    C Ca Cam Cam
  65. Cam searchIntent filter() flatMap() startWith( new Loading() ) L map(

    new SR() ) Cam C Ca Cam Cam
  66. Cam searchIntent filter() flatMap() startWith( new Loading() ) L map(

    new SR() ) Cam C Ca Cam Cam
  67. Cam searchIntent filter() flatMap() startWith( new Loading() ) L map(

    new SR() ) SR view.render( SearchViewState.SearchResult ) Cam C Ca Cam Cam
  68. Cam searchIntent filter() flatMap() startWith( new Loading() ) L map(

    new SR() ) SR view.render( SearchViewState.SearchResult ) Cam C Ca Cam Cam
  69. Cam searchIntent filter() flatMap() startWith( new Loading() ) L map(

    new SR() ) SR view.render( SearchViewState.SearchResult ) Cam C Ca Cam Cam
  70. None
  71. None
  72. None
  73. None
  74. interface HomeView { fun loadFirstPageIntent() : Observable<Unit> fun loadNextPageIntent() :

    Observable<Unit> fun pullToRefreshIntent() : Observable<Unit> fun loadAllProductsFromCategoryIntent() : Observable<String> fun render(viewState : HomeViewState) }
  75. class HomePresenter : MviBasePresenter<HomeView> { val HomeFeedLoader feedLoader; override fun

    bindIntents() { val firstPage : Observable<FeedLoaderState> = ... val pullToRefresh : Observable<FeedLoaderState> = ... val nextPage : Observable<FeedLoaderState> = ... val loadMoreFromCategory : Observable<FeedLoaderState> = ... val allIntents : Observable<FeedLoaderState> = Observable.merge(firstPage, pullToRefresh, nextPage, loadMoreFromCategory) stateObservable : Observable<HomeViewState> = allIntents.scan(this::reduceState) subscribeViewState(stateObservable, HomeView::render) }
  76. class HomePresenter : MviBasePresenter<HomeView> { val HomeFeedLoader feedLoader; override fun

    bindIntents() { val firstPage : Observable<FeedLoaderState> = ... val pullToRefresh : Observable<FeedLoaderState> = ... val nextPage : Observable<FeedLoaderState> = ... val loadMoreFromCategory : Observable<FeedLoaderState> = ... val allIntents : Observable<FeedLoaderState> = Observable.merge(firstPage, pullToRefresh, nextPage, loadMoreFromCategory) stateObservable : Observable<HomeViewState> = allIntents.scan(this::reduceState) subscribeViewState(stateObservable, HomeView::render) }
  77. class HomePresenter : MviBasePresenter<HomeView> { val HomeFeedLoader feedLoader; override fun

    bindIntents() { val firstPage : Observable<FeedLoaderState> = ... val pullToRefresh : Observable<FeedLoaderState> = ... val nextPage : Observable<FeedLoaderState> = ... val loadMoreFromCategory : Observable<FeedLoaderState> = ... val allIntents : Observable<FeedLoaderState> = Observable.merge(firstPage, pullToRefresh, nextPage, loadMoreFromCategory) stateObservable : Observable<HomeViewState> = allIntents.scan(this::reduceState) subscribeViewState(stateObservable, HomeView::render) }
  78. class HomePresenter : MviBasePresenter<HomeView> { val HomeFeedLoader feedLoader; override fun

    bindIntents() { val firstPage : Observable<FeedLoaderState> = ... val pullToRefresh : Observable<FeedLoaderState> = ... val nextPage : Observable<FeedLoaderState> = ... val loadMoreFromCategory : Observable<FeedLoaderState> = ... val allIntents : Observable<FeedLoaderState> = Observable.merge(firstPage, pullToRefresh, nextPage, loadMoreFromCategory) stateObservable : Observable<HomeViewState> = allIntents.scan(this::reduceState) subscribeViewState(stateObservable, HomeView::render) }
  79. fun reduceState ( previous : HomeViewState, feedLoaderState : FeedLoaderState )

    : HomeViewState { val newState : HomeViewState = … // compute the new State // by taking previous state and foo into account return newState; }
  80. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 State Driven by Business

    Logic
  81. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 Microservices

  82. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 state

  83. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 state state

  84. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017

  85. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 Independent UI Components

  86. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017

  87. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017

  88. interface SelectedCountToolbar { fun deleteSelectedIntent() : Observable<Unit> fun render(viewState :

    Int) }
  89. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 Reproducible States

  90. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017

  91. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017

  92. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 Testing

  93. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 assertEquals (expectedState, actualState)

  94. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 More Integration Tests Less

    Unit tests
  95. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 More Integration Tests Less

    Unit Tests
  96. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 Why Unit Tests -

    Test code in isolation - Reliable (not flaky) - Run fast - Testing UI as part of an Integration Test is senseless, since View is dump
  97. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 2 Unit Tests, 0

    Integration Test
  98. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 2 Unit Tests, 0

    Integration Test
  99. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 What if we architect

    and write Test Code the way we write production code?
  100. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 Let’s write reactive and

    reusable Integration Tests
  101. class SearchFragment : Fragment , SearchView { ... override fun

    searchIntent() : Observable<String> { return RxSearchView.queryTextChanges(searchView) } }
  102. class SearchFragment : Fragment , SearchView { ... override fun

    render( viewState : SearchViewState) { if (viewState.loading) renderLoading() else if (viewState.result != null) renderResult(viewState.result) else if (viewState.error != null) renderError() else throw IllegalStateException("Unknown ViewState") } }
  103. class SearchFragment : Fragment , SearchView { @Inject var viewBinding

    : SearchViewBinding override fun searchIntent() : Observable<String> { return viewBinding.searchIntent() } override fun render( viewState : SearchViewState) { viewBinding.render(viewState) } }
  104. class SearchViewBinding : SearchView { val rootView : ViewGroup override

    fun searchIntent() : Observable<String> { return RxSearchView.queryTextChanges(searchView) } override fun render( viewState : SearchViewState) { if (viewState.loading) … } }
  105. class TestSearchViewBinding : SearchViewBinding { override fun render( viewState :

    SearchViewState ) { super.render(viewState) } }
  106. class TestSearchViewBinding : SearchViewBinding { var statesList : List<SearchViewState> val

    renderedStates : BehaviorSubject<List<SearchViewState>> override fun render( viewState : SearchViewState ) { super.render(viewState) statesList = statesList.copyAndAdd(viewState) renderedStates.onNext(statesList) } }
  107. class TestSearchViewBinding : SearchViewBinding { var statesList : List<SearchViewState> val

    renderedStates : BehaviorSubject<List<SearchViewState>> override fun render( viewState : SearchViewState ) { super.render(viewState) statesList = statesList.copyAndAdd(viewState) renderedStates.onNext(statesList) Screenshot.snap(rootView).record() } } http://facebook.github.io/screenshot-tests-for-android
  108. class SearchFragmentTest { @Rule val rule = ActivityTestRule()
 val viewBinding

    : TestSearchViewBinding @Test fun onTyping3CharsShowLoadingThenResults() { onView(withId(…).perform(typeText(“Camera“) val states = viewBinding.renderedStates.blockingFirst() val loading = SearchViewState(true, null, null) val results = SearchViewState(false, someProducts, null) val expectedStates = listOf(loading, results) assertEquals(expectedStates, states) } }
  109. class SearchFragmentTest { @Rule val rule = ActivityTestRule()
 val viewBinding

    : TestSearchViewBinding @Test fun onTyping3CharsShowLoadingThenResults() { onView(withId(…).perform(typeText(“Camera“) val states = viewBinding.renderedStates.blockingFirst() val loading = SearchViewState(true, null, null) val results = SearchViewState(false, someProducts, null) val expectedStates = listOf(loading, results) assertEquals(expectedStates, states) } }
  110. class SearchFragmentTest { @Rule val rule = ActivityTestRule()
 val viewBinding

    : TestSearchViewBinding @Test fun onTyping3CharsShowLoadingThenResults() { onView(withId(…).perform(typeText(“Camera“) val states = viewBinding.renderedStates.blockingFirst() val loading = SearchViewState(true, null, null) val results = SearchViewState(false, someProducts, null) val expectedStates = listOf(loading, results) assertEquals(expectedStates, states) } }
  111. class SearchFragmentTest { @Rule val rule = ActivityTestRule()
 val viewBinding

    : TestSearchViewBinding @Test fun onTyping3CharsShowLoadingThenResults() { onView(withId(…).perform(typeText(“Camera“) val states = viewBinding.renderedStates.blockingFirst() val loading = SearchViewState(true, null, null) val results = SearchViewState(false, someProducts, null) val expectedStates = listOf(loading, results) assertEquals(expectedStates, states) } }
  112. class SearchFragmentTest { @Rule val rule = ActivityTestRule()
 val viewBinding

    : TestSearchViewBinding @Test fun onTyping3CharsShowLoadingThenResults() { onView(withId(…).perform(typeText(“Camera“) val states = viewBinding.renderedStates.blockingFirst() val loading = SearchViewState(true, null, null) val results = SearchViewState(false, someProducts, null) val expectedStates = listOf(loading, results) assertEquals(expectedStates, states) } } INPUT OUTPUT ASSERT.
  113. class Inputs (val viewBinding : TestSearchViewBinding) { fun writeIntoSearchBox( term

    : String) { onView(withId(…).perform(typeText(term) } }
  114. class Output (val viewBinding : TestSearchViewBinding) { fun renderedStates() :

    List<SearchViewState> = viewBinding.renderedStates.blockingFirst() }
  115. class Assertions (val inputs : Inputs, val output : Output)

    { fun loadThenShowResults() = Completable.fromCallable { inputs.writeIntoSearchBox(“Camera“) val states = output.renderedStates() val loading = SearchViewState(true, null, null) val results = SearchViewState(false, someProducts, null) val expectedStates = listOf(loading, results) assertEquals(expectedStates, states) } }
  116. class SearchFragmentTest { @Rule val rule = ActivityTestRule()
 val viewBinding

    : TestSearchViewBinding @Test fun onTyping3CharsShowLoadingThenResults() { val assertions = Assertions ( Inputs(viewBinding) , Output(viewBinding) ) assertions.loadThenShowResults().awaitBlocking() } }
  117. class FooAndBarTest { @Rule val rule = ActivityTestRule()
 val viewBinding

    : TestSearchViewBinding @Test fun fooAndBar() { val fooAssertions = FooAssertions (…) val barAssertions = BarAssertions (…) fooAssertions.something() .concatWith(barAssertions) .blockingAwait() } }
  118. class SearchFragmentTest { @Rule val rule = ActivityTestRule()
 val viewBinding

    : TestSearchViewBinding @Test fun onTyping3CharsShowLoadingThenResults() { val assertions = Assertions ( Inputs(viewBinding) , Output(viewBinding) ) assertions.loadThenShowResults().awaitBlocking() } }
  119. class JvmSearchViewTest { @Test fun onTyping3CharsShowLoadingThenResults() { val assertions =

    Assertions ( JvmInputs(realPresenter) , JvmOutput(realPresenter) ) assertions.loadThenShowResults().awaitBlocking() } }
  120. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 Why Unit Tests -

    Test code in isolation - Reliable (not flaky) - Run fast - Testing UI as part of an Integration Test is senseless, since View is dump
  121. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 Model-View-Intent - takes state

    management serious —> Solves & avoids a lot of problems - Unidirectional dataflow - State driven by business logic - Composable - Easy to test
  122. MOBICONF.ORG | 5-6 OCTOBER 2017 #mobiconf2017 Hannes Dorfmann @sockeqwe www.hannesdorfmann.com

    hannes@tickaroo.com
  123. & Q&A questions answers 112

  124. THANK YOU! 113