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

KotlinConf 2017: View State Machine For Network Calls on Android

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

  2. PRESENTER class MainPresenter(val view: MainView) { fun onCreate() { val

    iceCream = ?? view.showTitle(iceCream.title) view.showIcon(iceCream.iconUrl) } }
  3. 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) } }
  4. PRESENTER TEST class MainPresenterTest { val view = mock<MainView>() val

    dataStore = mock<DataStore>() 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) }
  5. PRESENTER TEST class MainPresenterTest { val view = mock<MainView>() val

    dataStore = mock<DataStore>() 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) }
  6. PRESENTER TEST class MainPresenterTest { val view = mock<MainView>() val

    dataStore = mock<DataStore>() 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) }
  7. PRESENTER TEST class MainPresenterTest { val view = mock<MainView>() val

    dataStore = mock<DataStore>() 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) }
  8. PRESENTER TEST class MainPresenterTest { val view = mock<MainView>() val

    dataStore = mock<DataStore>() 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) }
  9. PRESENTER TEST class MainPresenterTest { val view = mock<MainView>() val

    dataStore = mock<DataStore>() 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) }
  10. PRESENTER TEST class MainPresenterTest { val view = mock<MainView>() val

    dataStore = mock<DataStore>() 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) }
  11. 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 } ) } }
  12. 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 } ) } }
  13. 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 } ) } }
  14. 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 } ) } }
  15. 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 } ) } }
  16. VIEW INTERFACE + NETWORKING interface MainView { fun showTitle(title: String)

    fun showIcon(url: String) + fun showLoading() + fun hideLoading() + fun showError(errorMessage: String) }
  17. 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) } ) } }
  18. 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) }
  19. 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) }
  20. !

  21. UPDATES 1. Update our model 2. Update our view interface

    3. Update our presenter 4. Update presenter tests
  22. 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) }
  23. 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) } ) } }
  24. PRESENTER TEST + CALORIES class MainPresenterTest { val view =

    mock<MainView>() val dataStore = mock<DataStore>() 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) } ...
  25. 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
  26. 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
  27. NetworkingViewState sealed class NetworkingViewState { class Init() : NetworkingViewState() class

    Loading() : NetworkingViewState() class Success<out T>(val item: T) : NetworkingViewState() class Error(val errorMessage: String?) : NetworkingViewState() }
  28. 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 }
  29. 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>(iceCream) }, { error -> - view.hideLoading() - view.showError(error.message) + view.networkingViewState = NetworkingViewState.Error(error.message) } ) } }
  30. 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<NetworkingViewState.Loading>() + verify(view).networkingViewState = isA<NetworkingViewState.Success<IceCream>>() verifyNoMoreInteractions(view) } }
  31. 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<NetworkingViewState.Loading>() + verify(view).networkingViewState = isA<NetworkingViewState.Error(errorMessage) verifyNoMoreInteractions(view) } }
  32. NetworkingViewState sealed class NetworkingViewState { class Init() : NetworkingViewState() class

    Loading() : NetworkingViewState() class Success<out T>(val item: T) : NetworkingViewState() class Error(val errorMessage: String?) : NetworkingViewState() }
  33. 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>(iceCream) + view.networkingViewState = NetworkingViewState.Success<IceCreamViewModel>(IceCreamViewModel(iceCream, context)) }, { error -> view.networkingViewState = NetworkingViewState.Error(error.message) } ) } }
  34. 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) } }
  35. 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) } }
  36. !

  37. !

  38. 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) {} }
  39. 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) {} }
  40. ObservableProperty public abstract class ObservableProperty<T>(initialValue: T) : ReadWriteProperty<Any?, T> {

    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) } }
  41. ObservableProperty public abstract class ObservableProperty<T>(initialValue: T) : ReadWriteProperty<Any?, T> {

    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) } }
  42. ObservableProperty public abstract class ObservableProperty<T>(initialValue: T) : ReadWriteProperty<Any?, T> {

    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) } }
  43. 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<NetworkingViewState>( Init(), { property, oldValue, newValue -> }) }