VIEW STATE MACHINE FOR NETWORK CALLS ON ANDROID @MANDYBESS - - PowerPoint PPT Presentation

view state machine for network calls on android
SMART_READER_LITE
LIVE PREVIEW

VIEW STATE MACHINE FOR NETWORK CALLS ON ANDROID @MANDYBESS - - PowerPoint PPT Presentation

VIEW STATE MACHINE FOR NETWORK CALLS ON ANDROID @MANDYBESS THOUGHTBOT WHAT IS A VIEW STATE MACHINE? ! BUILD AN APP TOGETHER ! " GAME OF CONES ! MODELING !"! ICE CREAM - title - icon data class IceCream(val title: String, val


slide-1
SLIDE 1

VIEW STATE MACHINE FOR NETWORK CALLS ON ANDROID

@MANDYBESS

slide-2
SLIDE 2

THOUGHTBOT

slide-3
SLIDE 3

WHAT IS A VIEW STATE MACHINE?

slide-4
SLIDE 4

!

slide-5
SLIDE 5

BUILD AN APP TOGETHER ! "

slide-6
SLIDE 6

GAME OF CONES !

slide-7
SLIDE 7

MODELING !"!

slide-8
SLIDE 8

ICE CREAM

  • title
  • icon
slide-9
SLIDE 9

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

slide-10
SLIDE 10

! ➡ "

slide-11
SLIDE 11

MVP

MODEL VIEW PRESENTER

slide-12
SLIDE 12

VIEW INTERFACE

interface MainView { fun showTitle(title: String) fun showIcon(url: String) }

slide-13
SLIDE 13

PRESENTER

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

slide-14
SLIDE 14

DATA STORE

interface DataStore { fun fetchCone(): IceCream }

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

slide-16
SLIDE 16

TESTING !

slide-17
SLIDE 17

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) }
slide-18
SLIDE 18

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) }
slide-19
SLIDE 19

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) }
slide-20
SLIDE 20

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) }
slide-21
SLIDE 21

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) }
slide-22
SLIDE 22

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) }
slide-23
SLIDE 23

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) }
slide-24
SLIDE 24

NETWORK CALLS !"

slide-25
SLIDE 25

DATA STORE + RX

interface DataStore { + fun fetchCone(): Observable<IceCream> }

slide-26
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 } ) } }
slide-27
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 } ) } }
slide-28
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 } ) } }
slide-29
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 } ) } }
slide-30
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 } ) } }
slide-31
SLIDE 31

VIEW INTERFACE + NETWORKING

interface MainView { fun showTitle(title: String) fun showIcon(url: String) + fun showLoading() + fun hideLoading() + fun showError(errorMessage: String) }

slide-32
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) } ) } }
slide-33
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) }
slide-34
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) }
slide-35
SLIDE 35

WE DID IT !

slide-36
SLIDE 36
slide-37
SLIDE 37
slide-38
SLIDE 38

!

slide-39
SLIDE 39

UPDATES

  • 1. Update our model
  • 2. Update our view interface
  • 3. Update our presenter
  • 4. Update presenter tests
slide-40
SLIDE 40

MODEL UPDATE

data class IceCream(val title: String, val iconUrl: String, val calorieCount: Int)
slide-41
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) }

slide-42
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) } ) } }
slide-43
SLIDE 43

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) } ...
slide-44
SLIDE 44

PROGRESS REPORT !

slide-45
SLIDE 45
  • used THE MVP PATTERN
  • adeed NETWORKING
  • updated THE UI
slide-46
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

slide-47
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

slide-48
SLIDE 48

HELLO !

NetworkingViewState

slide-49
SLIDE 49

NetworkingViewState

sealed class NetworkingViewState { class Init() : NetworkingViewState() class Loading() : NetworkingViewState() class Success<out T>(val item: T) : NetworkingViewState() class Error(val errorMessage: String?) : NetworkingViewState() }

slide-50
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 }

slide-51
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>(iceCream) }, { error ->
  • view.hideLoading()
  • view.showError(error.message)
+ view.networkingViewState = NetworkingViewState.Error(error.message) } ) } }
slide-52
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<NetworkingViewState.Loading>() + verify(view).networkingViewState = isA<NetworkingViewState.Success<IceCream>>() verifyNoMoreInteractions(view) } }
slide-53
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<NetworkingViewState.Loading>() + verify(view).networkingViewState = isA<NetworkingViewState.Error(errorMessage) verifyNoMoreInteractions(view) } }
slide-54
SLIDE 54

OUR VIEW TESTS ARE HOMELESS !"

slide-55
SLIDE 55
slide-56
SLIDE 56

NetworkingViewState

sealed class NetworkingViewState { class Init() : NetworkingViewState() class Loading() : NetworkingViewState() class Success<out T>(val item: T) : NetworkingViewState() class Error(val errorMessage: String?) : NetworkingViewState() }

slide-57
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>(iceCream)
+ view.networkingViewState = NetworkingViewState.Success<IceCreamViewModel>(IceCreamViewModel(iceCream, context)) }, { error -> view.networkingViewState = NetworkingViewState.Error(error.message) } ) } }
slide-58
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) } }
slide-59
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) } }
slide-60
SLIDE 60

!

slide-61
SLIDE 61

!

slide-62
SLIDE 62

MAIN ACTIVITY

class MainActivity : AppCompatActivity(), MainView {

  • verride fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) }

  • verride var networkingViewState: NetworkingViewState

get() = TODO() set(value) {} }

slide-63
SLIDE 63

MAIN ACTIVITY

class MainActivity : AppCompatActivity(), MainView {

  • verride fun onCreate(savedInstanceState: Bundle?) {

super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) }

  • verride var networkingViewState: NetworkingViewState

get() = TODO() set(value) {} }

slide-64
SLIDE 64

HELLO ! PROPERTY DELEGATES

slide-65
SLIDE 65

Delegates.observable

slide-66
SLIDE 66

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) } }
slide-67
SLIDE 67

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) } }
slide-68
SLIDE 68

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) } }
slide-69
SLIDE 69

MAIN ACTIVITY + PROPERTY DELEGATE

class MainActivity : AppCompatActivity(), MainView {
  • verride fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) }
  • verride var networkingViewState: NetworkingViewState by Delegates.observable<NetworkingViewState>(
Init(), { property, oldValue, newValue -> }) }
slide-70
SLIDE 70
slide-71
SLIDE 71

! LINKS

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