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

KotlinConf 2017: View State Machine For Network Calls on Android

Amanda Hill
November 02, 2017

KotlinConf 2017: View State Machine For Network Calls on Android

Amanda Hill

November 02, 2017
Tweet

More Decks by Amanda Hill

Other Decks in Programming

Transcript

  1. VIEW STATE MACHINE FOR
    NETWORK CALLS ON ANDROID
    @MANDYBESS

    View Slide

  2. THOUGHTBOT

    View Slide

  3. WHAT IS A VIEW
    STATE MACHINE?

    View Slide

  4. !

    View Slide

  5. BUILD AN APP TOGETHER ! "

    View Slide

  6. GAME OF CONES !

    View Slide

  7. MODELING
    !"!

    View Slide

  8. ICE CREAM
    - title
    - icon

    View Slide

  9. data class IceCream(val title: String, val iconUrl: String)

    View Slide

  10. ! ➡ "

    View Slide

  11. MVP
    MODEL VIEW PRESENTER

    View Slide

  12. VIEW INTERFACE
    interface MainView {
    fun showTitle(title: String)
    fun showIcon(url: String)
    }

    View Slide

  13. PRESENTER
    class MainPresenter(val view: MainView) {
    fun onCreate() {
    val iceCream = ??
    view.showTitle(iceCream.title)
    view.showIcon(iceCream.iconUrl)
    }
    }

    View Slide

  14. DATA STORE
    interface DataStore {
    fun fetchCone(): IceCream
    }

    View Slide

  15. PRESENTER + DATA STORE
    + class MainPresenter(val view: MainView, val dataStore: DataStore) {
    fun onCreate() {
    + val iceCream = dataStore.fetchCone()
    view.showTitle(iceCream.title)
    view.showIcon(iceCream.iconUrl)
    }
    }

    View Slide

  16. TESTING !

    View Slide

  17. PRESENTER TEST
    class MainPresenterTest {
    val view = mock()
    val dataStore = mock()
    val fakeIceCream = IceCream("Vanilla", "www.icecream.com")
    @Test
    fun test_onCreate() {
    //stub response
    whenever(dataStore.fetchCone()).thenReturn(fakeIceCream)
    val presenter = MainPresenter(view, dataStore)
    presenter.onCreate()
    verify(view).showTitle("Vanilla")
    verify(view).showIcon("www.icecream.com")
    verifyNoMoreInteractions(view)
    }

    View Slide

  18. PRESENTER TEST
    class MainPresenterTest {
    val view = mock()
    val dataStore = mock()
    val fakeIceCream = IceCream("Vanilla", "www.icecream.com")
    @Test
    fun test_onCreate() {
    //stub response
    whenever(dataStore.fetchCone()).thenReturn(fakeIceCream)
    val presenter = MainPresenter(view, dataStore)
    presenter.onCreate()
    verify(view).showTitle("Vanilla")
    verify(view).showIcon("www.icecream.com")
    verifyNoMoreInteractions(view)
    }

    View Slide

  19. PRESENTER TEST
    class MainPresenterTest {
    val view = mock()
    val dataStore = mock()
    val fakeIceCream = IceCream("Vanilla", "www.icecream.com")
    @Test
    fun test_onCreate() {
    //stub response
    whenever(dataStore.fetchCone()).thenReturn(fakeIceCream)
    val presenter = MainPresenter(view, dataStore)
    presenter.onCreate()
    verify(view).showTitle("Vanilla")
    verify(view).showIcon("www.icecream.com")
    verifyNoMoreInteractions(view)
    }

    View Slide

  20. PRESENTER TEST
    class MainPresenterTest {
    val view = mock()
    val dataStore = mock()
    val fakeIceCream = IceCream("Vanilla", "www.icecream.com")
    @Test
    fun test_onCreate() {
    //stub response
    whenever(dataStore.fetchCone()).thenReturn(fakeIceCream)
    val presenter = MainPresenter(view, dataStore)
    presenter.onCreate()
    verify(view).showTitle("Vanilla")
    verify(view).showIcon("www.icecream.com")
    verifyNoMoreInteractions(view)
    }

    View Slide

  21. PRESENTER TEST
    class MainPresenterTest {
    val view = mock()
    val dataStore = mock()
    val fakeIceCream = IceCream("Vanilla", "www.icecream.com")
    @Test
    fun test_onCreate() {
    //stub response
    whenever(dataStore.fetchCone()).thenReturn(fakeIceCream)
    val presenter = MainPresenter(view, dataStore)
    presenter.onCreate()
    verify(view).showTitle("Vanilla")
    verify(view).showIcon("www.icecream.com")
    verifyNoMoreInteractions(view)
    }

    View Slide

  22. PRESENTER TEST
    class MainPresenterTest {
    val view = mock()
    val dataStore = mock()
    val fakeIceCream = IceCream("Vanilla", "www.icecream.com")
    @Test
    fun test_onCreate() {
    //stub response
    whenever(dataStore.fetchCone()).thenReturn(fakeIceCream)
    val presenter = MainPresenter(view, dataStore)
    presenter.onCreate()
    verify(view).showTitle("Vanilla")
    verify(view).showIcon("www.icecream.com")
    verifyNoMoreInteractions(view)
    }

    View Slide

  23. PRESENTER TEST
    class MainPresenterTest {
    val view = mock()
    val dataStore = mock()
    val fakeIceCream = IceCream("Vanilla", "www.icecream.com")
    @Test
    fun test_onCreate() {
    //stub response
    whenever(dataStore.fetchCone()).thenReturn(fakeIceCream)
    val presenter = MainPresenter(view, dataStore)
    presenter.onCreate()
    verify(view).showTitle("Vanilla")
    verify(view).showIcon("www.icecream.com")
    verifyNoMoreInteractions(view)
    }

    View Slide

  24. NETWORK CALLS !"

    View Slide

  25. DATA STORE + RX
    interface DataStore {
    + fun fetchCone(): Observable
    }

    View Slide

  26. PRESENTER + RX
    class MainPresenter(val view: MainView, val dataStore: DataStore) {
    fun onCreate() {
    dataStore.fetchCone()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    { iceCream ->
    // on success
    view.showTitle(iceCream.title)
    view.showIcon(iceCream.iconUrl)
    },
    { error ->
    // on error
    }
    )
    }
    }

    View Slide

  27. PRESENTER + RX
    class MainPresenter(val view: MainView, val dataStore: DataStore) {
    fun onCreate() {
    dataStore.fetchCone()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    { iceCream ->
    // on success
    view.showTitle(iceCream.title)
    view.showIcon(iceCream.iconUrl)
    },
    { error ->
    // on error
    }
    )
    }
    }

    View Slide

  28. PRESENTER + RX
    class MainPresenter(val view: MainView, val dataStore: DataStore) {
    fun onCreate() {
    dataStore.fetchCone()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    { iceCream ->
    // on success
    view.showTitle(iceCream.title)
    view.showIcon(iceCream.iconUrl)
    },
    { error ->
    // on error
    }
    )
    }
    }

    View Slide

  29. PRESENTER + RX
    class MainPresenter(val view: MainView, val dataStore: DataStore) {
    fun onCreate() {
    dataStore.fetchCone()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    { iceCream ->
    // on success
    view.showTitle(iceCream.title)
    view.showIcon(iceCream.iconUrl)
    },
    { error ->
    // on error
    }
    )
    }
    }

    View Slide

  30. PRESENTER + RX
    class MainPresenter(val view: MainView, val dataStore: DataStore) {
    fun onCreate() {
    // loading??
    dataStore.fetchCone()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    { iceCream ->
    // on success
    view.showTitle(iceCream.title)
    view.showIcon(iceCream.iconUrl)
    },
    { error ->
    // on error
    }
    )
    }
    }

    View Slide

  31. VIEW INTERFACE + NETWORKING
    interface MainView {
    fun showTitle(title: String)
    fun showIcon(url: String)
    + fun showLoading()
    + fun hideLoading()
    + fun showError(errorMessage: String)
    }

    View Slide

  32. PRESENTER + NETWORKING
    class MainPresenter(val view: MainView, val dataStore: DataStore) {
    fun onCreate() {
    + view.showLoading()
    dataStore.fetchCone()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    { iceCream ->
    + view.hideLoading()
    view.showTitle(iceCream.title)
    view.showIcon(iceCream.iconUrl)
    },
    { error ->
    + view.hideLoading()
    + view.showError(error.message)
    }
    )
    }
    }

    View Slide

  33. PRESENTER TEST + RX + NETWORKING
    ...
    @Test
    fun test_onCreate_success() {
    // stub response
    - whenever(dataStore.fetchCone()).thenReturn(fakeIceCream)
    + whenever(dataStore.fetchCone()).thenReturn(Observable.just(fakeIceCream))
    val presenter = MainPresenter(view, dataStore)
    presenter.onCreate()
    + verify(view).showLoading()
    + verify(view).hideLoading()
    verify(view).showTitle("Vanilla")
    verify(view).showIcon("www.icecream.com")
    verifyNoMoreInteractions(view)
    }

    View Slide

  34. PRESENTER TEST + RX + NETWORKING
    ...
    @Test
    fun test_onCreate_error() {
    // stub response
    val errorMessage = "There was an error"
    whenever(dataStore.fetchCone()).thenReturn(Observable.error(Throwable(errorMessage)))
    val presenter = MainPresenter(view, dataStore)
    presenter.onCreate()
    verify(view).showLoading()
    verify(view).hideLoading()
    verify(view).showError(errorMessage)
    verifyNoMoreInteractions(view)
    }

    View Slide

  35. WE DID IT !

    View Slide

  36. View Slide

  37. View Slide

  38. !

    View Slide

  39. UPDATES
    1. Update our model
    2. Update our view interface
    3. Update our presenter
    4. Update presenter tests

    View Slide

  40. MODEL UPDATE
    data class IceCream(val title: String, val iconUrl: String, val calorieCount: Int)

    View Slide

  41. VIEW INTERFACE + CALORIES
    interface MainView {
    fun showTitle(title: String)
    fun showIcon(url: String)
    fun showLoading()
    fun hideLoading()
    fun showError(errorMessage: String)
    + fun showCalorieCount(calorieCount: String)
    }

    View Slide

  42. PRESENTER + CALORIES
    class MainPresenter(val view: MainView, val dataStore: DataStore) {
    fun onCreate() {
    view.showLoading()
    dataStore.fetchCone()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    { iceCream ->
    view.hideLoading()
    view.showTitle(iceCream.title)
    view.showIcon(iceCream.iconUrl)
    + view.showCalorieCount(context.getString(R.string.formatted_calorie_count, iceCream.calorieCount))
    },
    { error ->
    view.hideLding()
    view.showError(error.message)
    }
    )
    }
    }

    View Slide

  43. PRESENTER TEST + CALORIES
    class MainPresenterTest {
    val view = mock()
    val dataStore = mock()
    val fakeIceCream = IceCream("Vanilla", "www.icecream.com", 120)
    @Test
    fun test_onCreate_success() {
    // stub response
    whenever(dataStore.fetchCone()).thenReturn(Observable.just(fakeIceCream))
    val presenter = MainPresenter(view, dataStore)
    presenter.onCreate()
    verify(view).showLoading()
    verify(view).hideLoading()
    verify(view).showTitle("Vanilla")
    verify(view).showIcon("www.icecream.com")
    + verify(view).showCalories("120 Calories")
    verifyNoMoreInteractions(view)
    }
    ...

    View Slide

  44. PROGRESS REPORT !

    View Slide

  45. - used THE MVP PATTERN
    - added NETWORKING
    - updated THE UI

    View Slide

  46. Pros Cons
    UI updates are caught in tests Updates to the Presenter violate
    Open-Closed principle
    Networking view functions take
    away from the bigger picture of
    what's happening

    View Slide

  47. Pros Cons
    UI updates are caught in tests Updates to the Presenter violate
    Open-Closed principle
    Networking view functions take
    away from the bigger picture of
    what's happening

    View Slide

  48. HELLO !
    NetworkingViewState

    View Slide

  49. NetworkingViewState
    sealed class NetworkingViewState {
    class Init() : NetworkingViewState()
    class Loading() : NetworkingViewState()
    class Success(val item: T) : NetworkingViewState()
    class Error(val errorMessage: String?) : NetworkingViewState()
    }

    View Slide

  50. MainView
    interface MainView {
    - fun showTitle(title: String)
    - fun showIcon(url: String)
    - fun showLoading()
    - fun hideLoading()
    - fun showError(errorMessage: String)
    - fun showCalorieCount(calorieCount: String)
    + var networkingViewState: NetworkingViewState
    }

    View Slide

  51. MainPresenter
    class MainPresenter(val view: MainView, val dataStore: DataStore) {
    fun onCreate() {
    - view.showLoading()
    + view.networkingViewState = NetworkingViewState.Loading()
    dataStore.fetchCone()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    { iceCream ->
    - view.hideLoading()
    - view.showTitle(iceCream.title)
    - view.showIcon(iceCream.iconUrl)
    - view.showCalorieCount(R.string.formatted_calorie_count, iceCream.calorieCount)
    + view.networkingViewState = NetworkingViewState.Success(iceCream)
    },
    { error ->
    - view.hideLoading()
    - view.showError(error.message)
    + view.networkingViewState = NetworkingViewState.Error(error.message)
    }
    )
    }
    }

    View Slide

  52. MainPresenterTest
    ...
    @Test
    fun test_onCreate_success() {
    // stub response
    whenever(dataStore.fetchCone()).thenReturn(Observable.just(fakeIceCream))
    val presenter = MainPresenter(view, dataStore)
    presenter.onCreate()
    - verify(view).showLoading()
    - verify(view).hideLoading()
    - verify(view).showTitle("Vanilla")
    - verify(view).showIcon("www.icecream.com")
    - verify(view).showCalorieCount(120 Calories)
    + verify(view).networkingViewState = isA()
    + verify(view).networkingViewState = isA>()
    verifyNoMoreInteractions(view)
    }
    }

    View Slide

  53. MainPresenterTest
    ...
    @Test
    fun test_onCreate_error() {
    // stub response
    val errorMessage = "There was an error"
    whenever(dataStore.fetchCone()).thenReturn(Observable.error(Throwable(errorMessage)))
    val presenter = MainPresenter(view, dataStore)
    presenter.onCreate()
    - verify(view).showLoading()
    - verify(view).hideLoading()
    - verify(view).showError()
    + verify(view).networkingViewState = isA()
    + verify(view).networkingViewState = isAverifyNoMoreInteractions(view)
    }
    }

    View Slide

  54. OUR VIEW TESTS ARE
    HOMELESS
    !"

    View Slide

  55. View Slide

  56. NetworkingViewState
    sealed class NetworkingViewState {
    class Init() : NetworkingViewState()
    class Loading() : NetworkingViewState()
    class Success(val item: T) : NetworkingViewState()
    class Error(val errorMessage: String?) : NetworkingViewState()
    }

    View Slide

  57. PRESENTER + VIEW MODEL
    class MainPresenter(val view: MainView, val dataStore: DataStore) {
    fun onCreate() {
    view.networkingViewState = NetworkingViewState.Loading()
    dataStore.fetchCone()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(
    { iceCream ->
    - view.networkingViewState = NetworkingViewState.Success(iceCream)
    + view.networkingViewState = NetworkingViewState.Success(IceCreamViewModel(iceCream, context))
    },
    { error ->
    view.networkingViewState = NetworkingViewState.Error(error.message)
    }
    )
    }
    }

    View Slide

  58. VIEW MODEL
    class IceCreamViewModel(val iceCream: IceCream, val context: Context) {
    fun title(): String {
    return iceCream.title
    }
    fun iconUrl(): String {
    return iceCream.iconUrl
    }
    fun calorieCount(): String {
    return context.getString(R.string.formatted_calorie_count, iceCream.calorieCount)
    }
    }

    View Slide

  59. VIEW MODEL TEST
    class IceCreamViewModelTest {
    val fakeIceCream = IceCream("Vanilla", "www.icecream.com", 120)
    val context: Context
    @Test
    fun testTitle() {
    val viewModel = IceCreamViewModel(context, fakeIceCream)
    val expected = "Vanilla"
    val actual = viewModel.title()
    assertEquals(expected, actual)
    }
    }

    View Slide

  60. !

    View Slide

  61. !

    View Slide

  62. MAIN ACTIVITY
    class MainActivity : AppCompatActivity(), MainView {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    }
    override var networkingViewState: NetworkingViewState
    get() = TODO()
    set(value) {}
    }

    View Slide

  63. MAIN ACTIVITY
    class MainActivity : AppCompatActivity(), MainView {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    }
    override var networkingViewState: NetworkingViewState
    get() = TODO()
    set(value) {}
    }

    View Slide

  64. HELLO !
    PROPERTY DELEGATES

    View Slide

  65. Delegates.observable

    View Slide

  66. ObservableProperty
    public abstract class ObservableProperty(initialValue: T) : ReadWriteProperty {
    private var value = initialValue
    protected open fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = true
    protected open fun afterChange (property: KProperty<*>, oldValue: T, newValue: T): Unit {}
    public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
    return value
    }
    public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
    val oldValue = this.value
    if (!beforeChange(property, oldValue, value)) {
    return
    }
    this.value = value
    afterChange(property, oldValue, value)
    }
    }

    View Slide

  67. ObservableProperty
    public abstract class ObservableProperty(initialValue: T) : ReadWriteProperty {
    private var value = initialValue
    protected open fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = true
    protected open fun afterChange (property: KProperty<*>, oldValue: T, newValue: T): Unit {}
    public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
    return value
    }
    public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
    val oldValue = this.value
    if (!beforeChange(property, oldValue, value)) {
    return
    }
    this.value = value
    afterChange(property, oldValue, value)
    }
    }

    View Slide

  68. ObservableProperty
    public abstract class ObservableProperty(initialValue: T) : ReadWriteProperty {
    private var value = initialValue
    protected open fun beforeChange(property: KProperty<*>, oldValue: T, newValue: T): Boolean = true
    protected open fun afterChange (property: KProperty<*>, oldValue: T, newValue: T): Unit {}
    public override fun getValue(thisRef: Any?, property: KProperty<*>): T {
    return value
    }
    public override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
    val oldValue = this.value
    if (!beforeChange(property, oldValue, value)) {
    return
    }
    this.value = value
    afterChange(property, oldValue, value)
    }
    }

    View Slide

  69. MAIN ACTIVITY + PROPERTY DELEGATE
    class MainActivity : AppCompatActivity(), MainView {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    }
    override var networkingViewState: NetworkingViewState by Delegates.observable(
    Init(),
    { property, oldValue, newValue ->
    })
    }

    View Slide

  70. View Slide

  71. ! LINKS
    ! @mandybess
    " www.mandybess.com
    # https://robots.thoughtbot.com/android-networking-view-state
    $ https://speakerdeck.com/mandybess

    View Slide