$30 off During Our Annual Pro Sale. View Details »

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

    View Slide

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

    View Slide

  3. MOBICONF.ORG | 5-6 OCTOBER 2017
    #mobiconf2017
    Rainer Weiss Kip Thorne Barry Barish
    Nobel laureates in Physics 2017
    Gravitational waves

    View Slide

  4. MOBICONF.ORG | 5-6 OCTOBER 2017
    #mobiconf2017
    General relativity

    View Slide

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

    View Slide

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

    View Slide

  7. MOBICONF.ORG | 5-6 OCTOBER 2017
    #mobiconf2017
    2 black holes melded 1.3 Billion years ago
    Gravitational waves detected

    View Slide

  8. 8
    REACTIVE
    STATEMANAGEMENT
    WITH MODEL-VIEW-INTENT

    View Slide

  9. 9
    HELLO
    I’M HANNES

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  18. class PersonsPresenter : Presenter {
    fun loadPersons(){
    view?.showLoading(true) // Displays a ProgressBar
    backend.loadPersons({ persons : List ->
    view?.showPersons(persons) // Displays a ProgressBar
    },
    { error: Throwable ->
    view?.showError(error) // Displays a error message
    })
    }
    }

    View Slide

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

    View Slide

  20. class PersonsViewModel : ViewModel {
    val loadingLiveData : MutableLiveData

    fun loadPersons(){
    loadingLiveData.setValue((true)
    backend.loadPersons({ persons : List ->
    loadingLiveData.setValue( false )
    personsLiveData.setValue( persons )
    },
    { error: Throwable ->
    loadingLiveData.setValue( false )
    errorLiveData.setValue( error ) })
    }
    }

    View Slide

  21. class PersonsViewModel : ViewModel {
    val loadingLiveData : MutableLiveData

    fun loadPersons(){
    loadingLiveData.setValue((true)
    backend.loadPersons({ persons : List ->
    loadingLiveData.setValue( false )
    personsLiveData.setValue( persons )
    },
    { error: Throwable ->
    loadingLiveData.setValue( false )
    errorLiveData.setValue( error ) })
    }
    }

    View Slide

  22. class PersonsViewModel : ViewModel {
    val loadingLiveData : MutableLiveData

    fun loadPersons(){
    loadingLiveData.setValue((true)
    backend.loadPersons({ persons : List ->
    loadingLiveData.setValue( false )
    personsLiveData.setValue( persons )
    },
    { error: Throwable ->
    loadingLiveData.setValue( false )
    errorLiveData.setValue( error ) })
    }
    }

    View Slide

  23. class PersonsViewModel : ViewModel {
    val loadingLiveData : MutableLiveData

    fun loadPersons(){
    loadingLiveData.setValue((true)
    backend.loadPersons({ persons : List ->
    loadingLiveData.setValue( false )
    personsLiveData.setValue( persons )
    },
    { error: Throwable ->
    loadingLiveData.setValue( false )
    errorLiveData.setValue( error ) })
    }
    }

    View Slide

  24. class PersonsViewModel : ViewModel {
    val loadingLiveData : MutableLiveData

    fun loadPersons(){
    loadingLiveData.setValue((true)
    backend.loadPersons({ persons : List ->
    loadingLiveData.setValue( false )
    personsLiveData.setValue( persons )
    },
    { error: Throwable ->
    loadingLiveData.setValue( false )
    errorLiveData.setValue( error ) })
    }
    }

    View Slide

  25. class PersonsViewModel : ViewModel {
    val loadingLiveData : MutableLiveData

    fun loadPersons(){
    loadingLiveData.setValue((true)
    backend.loadPersons({ persons : List ->
    loadingLiveData.setValue( false )
    personsLiveData.setValue( persons )
    },
    { error: Throwable ->
    loadingLiveData.setValue( false )
    errorLiveData.setValue( error ) })
    }
    }

    View Slide

  26. class PersonsViewModel : ViewModel {
    val loadingLiveData : MutableLiveData

    fun loadPersons(){
    loadingLiveData.setValue((true)
    backend.loadPersons({ persons : List ->
    loadingLiveData.setValue( false )
    personsLiveData.setValue( persons )
    },
    { error: Throwable ->
    loadingLiveData.setValue( false )
    errorLiveData.setValue( error ) })
    }
    }

    View Slide

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

    View Slide

  28. data class PersonsModel (
    val loading : boolean,
    val persons : List?,
    val error : Throwable?
    )

    View Slide

  29. class PersonsPresenter : Presenter {
    fun loadPersons(){
    view?.render(PersonsModel ( true, null, null) )
    backend.loadPersons({ persons : List ->
    view?.render(PersonsModel ( false, persons, null) )
    },
    { error: Throwable ->
    view?.render(PersonsModel ( false, null, error) )
    })
    }
    }

    View Slide

  30. MOBICONF.ORG | 5-6 OCTOBER 2017
    #mobiconf2017
    Model == State

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  37. MOBICONF.ORG | 5-6 OCTOBER 2017
    #mobiconf2017
    Model-View-Intent

    View Slide

  38. View Slide

  39. View Slide

  40. interface SearchView {
    fun searchIntent() : Observable
    fun render(viewState : SearchViewState)
    }

    View Slide

  41. data class SearchViewState (
    val error : Throwable?
    val loading: boolean
    val result : List?
    )

    View Slide

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

    View Slide

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

    View Slide

  44. class SearchPresenter : MviBasePresenter {
    val SearchInteractor searchInteractor;
    override fun bindIntents() {
    val searchState : Observable =
    intent(SearchView::searchIntent)
    .filter { term -> term.size >= 3 }
    .switchMap(searchInteractor::search)
    subscribeViewState(searchState, SearchView::render)
    }
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  50. Search …
    searchIntent
    view.render( model )

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  70. View Slide

  71. View Slide

  72. View Slide

  73. View Slide

  74. interface HomeView {
    fun loadFirstPageIntent() : Observable
    fun loadNextPageIntent() : Observable
    fun pullToRefreshIntent() : Observable
    fun loadAllProductsFromCategoryIntent() : Observable
    fun render(viewState : HomeViewState)
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  80. MOBICONF.ORG | 5-6 OCTOBER 2017
    #mobiconf2017
    State Driven by Business Logic

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  88. interface SelectedCountToolbar {
    fun deleteSelectedIntent() : Observable
    fun render(viewState : Int)
    }

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

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

    View Slide

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

    View Slide

  99. MOBICONF.ORG | 5-6 OCTOBER 2017
    #mobiconf2017
    What if we architect and write Test Code
    the way we write production code?

    View Slide

  100. MOBICONF.ORG | 5-6 OCTOBER 2017
    #mobiconf2017
    Let’s write reactive and reusable
    Integration Tests

    View Slide

  101. class SearchFragment : Fragment , SearchView {
    ...
    override fun searchIntent() : Observable {
    return RxSearchView.queryTextChanges(searchView)
    }
    }

    View Slide

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

    View Slide

  103. class SearchFragment : Fragment , SearchView {
    @Inject var viewBinding : SearchViewBinding
    override fun searchIntent() : Observable {
    return viewBinding.searchIntent()
    }
    override fun render( viewState : SearchViewState) {
    viewBinding.render(viewState)
    }
    }

    View Slide

  104. class SearchViewBinding : SearchView {
    val rootView : ViewGroup
    override fun searchIntent() : Observable {
    return RxSearchView.queryTextChanges(searchView)
    }
    override fun render( viewState : SearchViewState) {
    if (viewState.loading)

    }
    }

    View Slide

  105. class TestSearchViewBinding : SearchViewBinding {
    override fun render( viewState : SearchViewState ) {
    super.render(viewState)
    }
    }

    View Slide

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

    View Slide

  107. class TestSearchViewBinding : SearchViewBinding {
    var statesList : List
    val renderedStates : BehaviorSubject>
    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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  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.

    View Slide

  113. class Inputs (val viewBinding : TestSearchViewBinding) {
    fun writeIntoSearchBox( term : String) {
    onView(withId(…).perform(typeText(term)
    }
    }

    View Slide

  114. class Output (val viewBinding : TestSearchViewBinding) {
    fun renderedStates() : List =
    viewBinding.renderedStates.blockingFirst()
    }

    View Slide

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

    View Slide

  116. class SearchFragmentTest {
    @Rule val rule = ActivityTestRule()

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

    View Slide

  117. class FooAndBarTest {
    @Rule val rule = ActivityTestRule()

    val viewBinding : TestSearchViewBinding
    @Test fun fooAndBar() {
    val fooAssertions = FooAssertions (…)
    val barAssertions = BarAssertions (…)
    fooAssertions.something()
    .concatWith(barAssertions)
    .blockingAwait()
    }
    }

    View Slide

  118. class SearchFragmentTest {
    @Rule val rule = ActivityTestRule()

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

    View Slide

  119. class JvmSearchViewTest {
    @Test fun onTyping3CharsShowLoadingThenResults() {
    val assertions = Assertions (
    JvmInputs(realPresenter) , JvmOutput(realPresenter)
    )
    assertions.loadThenShowResults().awaitBlocking()
    }
    }

    View Slide

  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

    View Slide

  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

    View Slide

  122. MOBICONF.ORG | 5-6 OCTOBER 2017
    #mobiconf2017
    Hannes Dorfmann
    @sockeqwe
    www.hannesdorfmann.com
    [email protected]

    View Slide

  123. &
    Q&A
    questions answers
    112

    View Slide

  124. THANK YOU!
    113

    View Slide