VIEW STATE MACHINE FOR NETWORK CALLS ON ANDROID
@MANDYBESS
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
@MANDYBESS
THOUGHTBOT
BUILD AN APP TOGETHER ! "
GAME OF CONES !
data class IceCream(val title: String, val iconUrl: String)
VIEW INTERFACE
interface MainView { fun showTitle(title: String) fun showIcon(url: String) }
PRESENTER
class MainPresenter(val view: MainView) { fun onCreate() { val iceCream = ?? view.showTitle(iceCream.title) view.showIcon(iceCream.iconUrl) } }
DATA STORE
interface DataStore { fun fetchCone(): IceCream }
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) } }
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) }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) }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) }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) }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) }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) }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) }DATA STORE + RX
interface DataStore { + fun fetchCone(): Observable<IceCream> }
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 } ) } }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 } ) } }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 } ) } }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 } ) } }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 INTERFACE + NETWORKING
interface MainView { fun showTitle(title: String) fun showIcon(url: String) + fun showLoading() + fun hideLoading() + fun showError(errorMessage: String) }
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) } ) } }PRESENTER TEST + RX + NETWORKING
... @Test fun test_onCreate_success() { // stub responsePRESENTER 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) }UPDATES
MODEL UPDATE
data class IceCream(val title: String, val iconUrl: String, val calorieCount: Int)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) }
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) } ) } }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) } ...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
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
HELLO !
NetworkingViewState
NetworkingViewState
sealed class NetworkingViewState { class Init() : NetworkingViewState() class Loading() : NetworkingViewState() class Success<out T>(val item: T) : NetworkingViewState() class Error(val errorMessage: String?) : NetworkingViewState() }
MainView
interface MainView {
+ var networkingViewState: NetworkingViewState }
MainPresenter
class MainPresenter(val view: MainView, val dataStore: DataStore) { fun onCreate() {MainPresenterTest
... @Test fun test_onCreate_success() { // stub response whenever(dataStore.fetchCone()).thenReturn(Observable.just(fakeIceCream)) val presenter = MainPresenter(view, dataStore) presenter.onCreate()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()NetworkingViewState
sealed class NetworkingViewState { class Init() : NetworkingViewState() class Loading() : NetworkingViewState() class Success<out T>(val item: T) : NetworkingViewState() class Error(val errorMessage: String?) : NetworkingViewState() }
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 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 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) } }MAIN ACTIVITY
class MainActivity : AppCompatActivity(), MainView {
super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) }
get() = TODO() set(value) {} }
MAIN ACTIVITY
class MainActivity : AppCompatActivity(), MainView {
super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) }
get() = TODO() set(value) {} }
Delegates.observable
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) } }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) } }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) } }MAIN ACTIVITY + PROPERTY DELEGATE
class MainActivity : AppCompatActivity(), MainView {! LINKS
! @mandybess " www.mandybess.com # https://robots.thoughtbot.com/android-networking-view-state $ https://speakerdeck.com/mandybess