Share
Embed
Transcript
126 slides
-
class PersonsPresenter : Presenter<PersonsView> { fun loadPersons(){ view?.showLoading(true) backend.loadPersons({ persons
: List<Person> → view?.showLoading(false) view?.showPersons(persons) }, { error: Throwable → view?.showLoading(false) view?.showError(error) }) } } -
class PersonsPresenter : Presenter<PersonsView> { fun loadPersons(){ view?.showLoading(true) backend.loadPersons({ persons
: List<Person> → view?.showLoading(false) view?.showPersons(persons) }, { error: Throwable → view?.showLoading(false) view?.showError(error) }) } } -
class PersonsViewModel : ViewModel { val loading : LiveData<Boolean> val
persons : LiveData<List<Person>> val error : LiveData<Throwable> fun loadPersons(){ loading.setValue( true ) backend.loadPersons({ p : List<Person> → loading.setValue( false ) persons.setValue( p ) }, { e : Throwable → loading.setValue( false ) error.setValue( e ) }) } -
class PersonsViewModel : ViewModel { val loading : LiveData<Boolean> val
persons : LiveData<List<Person>> val error : LiveData<Throwable> fun loadPersons(){ loading.setValue( true ) backend.loadPersons({ p : List<Person> → loading.setValue( false ) persons.setValue( p ) }, { e : Throwable → loading.setValue( false ) error.setValue( e ) }) } -
public TrainingSpotsPresenter(TrainingSpotsMvp.View view, TrainingSpotsMvp.Model model, NetworkManager networkManager, FreeleticsTracking tracking, ScreenTrackingDelegate
screenTrackingDelegate, EventBuildConfigInfo eventBuildConfigInfo) { this.model = model; this.view = view; this.networkManager = networkManager; this.tracking = tracking; this.screenTrackingDelegate = screenTrackingDelegate; this.eventBuildConfigInfo = eventBuildConfigInfo; subscriptions = new CompositeDisposable(); } @Override public void setTrackingScreenName() { screenTrackingDelegate.setScreenName(tracking, TrainingSpotsEvents.TRAINING_SPOT_LIST_PAGE_ID); } @Override public void loadTrainingSpots() { if (!networkManager.isOnline()) { view.showNoInternetConnection(); return; } view.showProgress(true); if (model.hasNoGpsPermissions()) { loadDefaultTrainingSpots(); } else { subscriptions.add(model.checkForHighAccuracy() .subscribe(status -> loadDefaultTrainingSpots(), throwable -> loadDefaultTrainingSpots(), () -> subscriptions.add( model.getNextTrainingSpots() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(trainingSpots -> { view.showProgress(false); view.showLocalTrainingSpots(trainingSpots, model.getNearbyTrainingSpotThreshold()); view.showShareNearbyTrainingSpotBanner( model.shouldShowBanner(trainingSpots)); tracking.trackEvent( TrainingSpotsEvents .pageImpressionTrainingSpotsList( eventBuildConfigInfo, true, trainingSpots)); -
model.shouldShowBanner(trainingSpots)); tracking.trackEvent( TrainingSpotsEvents .pageImpressionTrainingSpotsList( eventBuildConfigInfo, true, trainingSpots)); view.enablePaging(true); }, throwable
-> { view.showProgress(false); if (throwable instanceof TimeoutException) { view.showTimeoutMessage(); } else { view.showConnectionError(); } })))); } } private void loadDefaultTrainingSpots() { subscriptions.add(model.getDefaultTrainingSpots() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(trainingSpots -> { view.showProgress(false); view.showDefaultTrainingSpots(trainingSpots); tracking.trackEvent(TrainingSpotsEvents.pageImpressionTrainingSpotsList( eventBuildConfigInfo, false, trainingSpots)); view.enablePaging(false); }, throwable -> { view.showProgress(false); view.showConnectionError(); })); } @Override public void loadMoreTrainingSpots() { if (!networkManager.isOnline()) { view.showNoInternetConnectionMessage(); view.enablePaging(false); return; } view.showLoadingMoreTrainingSpotsProgress(true); Disposable disposable = model.getNextTrainingSpots() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(trainingSpots -> { view.showLoadingMoreTrainingSpotsProgress(false); -
.subscribe(trainingSpots -> { view.showLoadingMoreTrainingSpotsProgress(false); view.showMoreLocalTrainingSpots(trainingSpots, model.getNearbyTrainingSpotThreshold()); view.showShareNearbyTrainingSpotBanner(model.shouldShowBanner(trainingSpots)); }, throwable ->
{ view.showLoadingMoreTrainingSpotsProgress(false); view.showConnectionErrorMessage(); view.enablePaging(false); }); subscriptions.add(disposable); } @Override public void handleTrainingSpotSelection(TrainingSpot trainingSpot) { tracking.trackEvent(TrainingSpotsEvents.trainingSpotDetails(eventBuildConfigInfo, trainingSpot.id())); view.showTrainingSpotDetails(model.getLastKnownUserLocation(), trainingSpot); } @Override public void handleShareNearbyTrainingSpotAction() { view.openShareNearbySpotForm(model.getFormUrl()); } @Override public void handleFooterLocationAction() { if (model.hasNoGpsPermissions()) { view.showGpsPermissionDialog(GPS_PERMISSIONS_REQUEST_CODE); } else { checkForHighAccuracy(); } } @Override public void handleDisclaimerAction() { view.showDisclaimerPopUp(); } @Override public void handleChangeLocationSettingsResult(boolean success) { if (success) { if (model.hasNoGpsPermissions()) { view.showGpsPermissionDialog(GPS_PERMISSIONS_REQUEST_CODE); } else { loadTrainingSpots(); } } else { view.showEnableHighAccuracyModeErrorDialog(); } -
sealed class State { object Loading : State() data class
Content(val persons : List<Person>) : State() data class Error(val error : Throwable) : State() } -
Simple State Machines val httpRequest : Observable<List<Person>> = ... val
state : Observable<State> = ??? -
sealed class State { object Loading : State() data class
Content(val persons : List<Person>) : State() data class Error(val error : Throwable) : State() } -
Simple State Machines val httpRequest : Observable<List<Person>> = ... val
state : Observable<State> = httpRequest .map { persons → State.Content(persons) } .onErrorReturn { State.Error(it) } .startWith(State.Loading) -
Simple State Machines val httpRequest : Observable<List<Person>> = ... val
state : Observable<State> = httpRequest .map { persons → State.Content(persons) } .onErrorReturn { State.Error(it) } .startWith(State.Loading) -
Simple State Machines val httpRequest : Observable<List<Person>> = ... val
state : Observable<State> = httpRequest .map { persons → State.Content(persons) } .onErrorReturn { State.Error(it) } .startWith(State.Loading) -
Simple State Machines val sm1: Observable<State> = … val sm2:
Observable<State> = ... val combined: Observable<State> = Observable.zip(sm1, sm2){ s1,s2 → ... }) -
Simple State Machines val sm1: Observable<State> = … val sm2:
Observable<State> = ... val combined: Observable<State> = Observable.zip(sm1, sm2){ s1,s2 → if (s1 is State.Error) s1 ... }) -
Simple State Machines val sm1: Observable<State> = … val sm2:
Observable<State> = ... val combined: Observable<State> = Observable.zip(sm1, sm2){ s1,s2 → if (s1 is State.Error) s1 if (s2 is State.Error) s2 ... }) -
Simple State Machines val sm1: Observable<State> = … val sm2:
Observable<State> = ... val combined: Observable<State> = Observable.zip(sm1, sm2){ s1,s2 → if (s1 is State.Error) s1 if (s2 is State.Error) s2 if (s1 == State.Loading || s2 == State.Loading) State.Loading ... }) -
Simple State Machines val http1: Observable<List<Person>> = … val http2:
Observable<List<Person>> = ... val combined: Observable<State> = Observable.zip(http1, http2 ){ p1,p2 → p1 + p2 }) .map { persons → State.Content(persons) } .onErrorReturn { State.Error(it) } .startWith(State.Loading) -
class CalculatorStateMachine { sealed class Input { data class Add(val
value: Int) : Input() data class Sub(val value: Int) : Input() data class Mul(val value: Int) : Input() data class Div(val value: Int) : Input() } } -
class CalculatorStateMachine { sealed class Input { ... } sealed
class State { data class Result(val value: Int) : State() data class Error(val error: Throwable) : State() } } -
class CalculatorStateMachine { sealed class Input { ... } sealed
class State { ... } private val inputRelay = PublishRelay.create<Input>() } -
class CalculatorStateMachine { sealed class Input { ... } sealed
class State { ... } private val inputRelay = PublishRelay.create<Input>() val input : Consumer<Input> = inputRelay } -
class CalculatorStateMachine { sealed class Input { ... } sealed
class State { ... } private val inputRelay = PublishRelay.create<Input>() val input : Consumer<Input> = inputRelay val state: Observable<State> = inputRelay.map { ... } } -
class CalculatorStateMachine { sealed class Input { ... } sealed
class State { ... } private val inputRelay = PublishRelay.create<Input>() val input : Consumer<Input> = inputRelay val state: Observable<State> = inputRelay.map { ... } } -
Fancy Redux Based State Machines - Store → Observable for
State - Actions as input - Reducer: (State , Action ) → State - Isolate Side Effects -
View ViewModel / Presenter Redux State Machine Intent Action State
View State Action triggered by side effect -
View Redux State Machine reducer(state, action) : State nextPage(nextPageAction) :
Action State Next page -
View Redux State Machine reducer(state, action) : State nextPage(nextPageAction) :
Action State Next page Next page -
View Redux State Machine reducer(state, action) : State nextPage(nextPageAction) :
Action State Next page Next page -
View Redux State Machine reducer(state, action) : State nextPage(nextPageAction) :
Action State Next page -
View Redux State Machine reducer(state, action) : State nextPage(nextPageAction) :
Action State LoadingPage -
View Redux State Machine reducer(state, action) : State nextPage(nextPageAction) :
Action State LoadingPage -
View Redux State Machine reducer(state, action) : State nextPage(nextPageAction) :
Action State LoadingPage -
View Redux State Machine reducer(state, action) : State nextPage(nextPageAction) :
Action State ResultNextPage -
View Redux State Machine reducer(state, action) : State nextPage(nextPageAction) :
Action State ResultNextPage -
View Redux State Machine reducer(state, action) : State nextPage(nextPageAction) :
Action State ResultNextPage -
View Redux State Machine reducer(state, action) : State Side Effect
1 State Side Effect 2 Side Effect n ... -
val sideEffects = listOf< SideEffect<State, Action> >( ... nextPageSideEffect, pullToRefreshSideEffect,
... ) intents .reduxStore(initialState, sideEffects, reducer) .subscribe { state → view.render(state) } https://github.com/freeletics/RxRedux -
Testing is easy - assert(expectedState, actualState) - Just trigger intent,
wait for state change - No need for idling resource needed - Prefer integration testing over unit testing -
class MyActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) {
... val button = findViewById(R.id.button) ... } fun render(state: State) { ... } } -
class MyActivity : Activity() { @Inject lateinit var binding :
ViewBinding override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // inject binding ... } fun render(state: State) { binding.render() } } -
class ViewBinding(val rootView : View) { val button = rootView.findViewById(R.id.button)
fun render(state: State) { ... } } -
class TestingViewBinding(rootView : View) : ViewBinding { val renderdStates :
Observable<State> = replay private val replay = ReplayRelay.create<State>() override fun render(state: State) { super.render(state) replay.onNext(state) Screenshot.snap(rootView).record() } } -
class TestingViewBinding(rootView : View) : ViewBinding { val renderdStates :
Observable<State> = replay private val replay = ReplayRelay.create<State>() override fun render(state: State) { super.render(state) replay.onNext(state) Screenshot.snap(rootView).record() } } -
class TestingViewBinding(rootView : View) : ViewBinding { val renderdStates :
Observable<State> = replay private val replay = ReplayRelay.create<State>() override fun render(state: State) { super.render(state) replay.onNext(state) Screenshot.snap(rootView).record() } } -
class TestingViewBinding(rootView : View) : ViewBinding { val renderdStates :
Observable<State> = replay private val replay = ReplayRelay.create<State>() override fun render(state: State) { super.render(state) replay.onNext(state) Screenshot.snap(rootView).record() } } -
class TestingViewBinding(rootView : View) : ViewBinding { val renderdStates :
Observable<State> = replay private val replay = ReplayRelay.create<State>() override fun render(state: State) { super.render(state) replay.onNext(state) Screenshot.snap(rootView).record() } } https://facebook.github.io/screenshot-tests-for-android/ -
intents .share() .subscribe { intent -> when (intent) { NextButtonClick
-> tracker.trackNextButtonClick() ... } } -
stateMachine .share() .subscribe { state -> when (state) { is
StateA -> tracker.trackStateA() ... } } -
sealed class State { data class UserData( val login: String,
val password: String ) : State() data class SingingIn( val login: String, val password: String ) : State() data class Error( val login: String, val password: String, val errorMessage: String, ) : State() object SignedIn : State() } -
sealed class State { data class UserData( val login: String,
val password: String ) : State() data class SingingIn( val login: String, val password: String ) : State() data class Error( val login: String, val password: String, val errorMessage: String, ) : State() object SignedIn : State( } fun render(state: State) { when (state) { State.SignedIn -> { startActivity(nextActivityIntent()) finish() } ... } } -
Navigator stateMachine .share() .subscribe { state -> when (state) {
... -> navigator.navigateTo(Destination.OptionalQuestions) } } -
Navigator stateMachine .share() .subscribe { state -> when (state) {
... -> navigator.navigateTo(Destination.OptionalQuestions) } } class WizardNavigator( private val activity: Activity, private val abTestProvider: AbTestProvider ) : Navigator { override fun navigateTo(dest: Destination) { when (dest) { Destination.Form -> showFormFragment() Destination.OptionalQuestions -> showOptionalQuestionsFragment() Destination.VariantSelector -> showVariantSelectorFragment() Destination.FromSaved -> activity.startActivity(abTestProvider.nextScreen()) } } } -
Navigator implementation Deep inside, the Navigator implementation could be as
simple as override fun navigateTo(dest: Destination) { when (dest) { Destination.Next -> showNextActivity() ... } } fun showNextActivity() { activity.startActivity( if (FeatureFlag.A) { activityOneIntent() } else { activityTwoIntent() } ) } -
Navigator implementation Or be little bit modern override fun navigateTo(dest:
Destination) { when (dest) { Destination.Next -> showNextActivity() ... } } fun showNextActivity() { view.findNavController().navigate(R.id.wizard_next_activity) } -
Animation private sealed class InternalAnimationState { object InitialState : InternalAnimationState()
object ProgressTransition : InternalAnimationState() data class Progress( val textResId: Int, val currentProgressPercent: Int, val totalProgressDuration: Long ) : InternalAnimationState() object GenerationFinished : InternalAnimationState() } -
Animation data class Progress( val textResId: Int, val currentProgressPercent: Int,
val totalProgressDuration: Long ) :InternalAnimationState() -
Animation private fun createAnimationStateObservable(): Observable<InternalAnimationState> = Observable.merge( intents.filter { it
== GenerationClicked } .map { ProgressTransition }, intents.filter { it == GenerationStarted } .flatMap { Observable.intervalRange( 0, steps, 0, duration / steps, TimeUnit.MILLISECONDS ).map { it.toInt() } .map { if (step == size) { GenerationFinished } else { Progress(...) } } }) -
Animation private fun createAnimationState(): Observable<InternalAnimationState> = rxObservable { intents.consumeEach {
intent -> when (intent) { GenerationClicked -> send(ProgressTransition) GenerationStarted -> { repeat(size) { step -> delay(duration / size) send(if (step == texts.size) { GenerationFinished } else { Progress(...) }) } } } } } -
Animation private fun createAnimationState(): Observable<InternalAnimationState> = rxObservable { intentStream.consumeEach {
intent -> when (intent) { GenerationClicked -> send(ProgressTransition) GenerationStarted -> { repeat(size) { step -> delay(duration / size) send(if (step == texts.size) { GenerationFinished } else { Progress(...) }) } } } } } -
enum class Variant { Variant1, Variant2 } data class Step1State(
val selected: Variant ) -
enum class Variant { Variant1, Variant2 } data class Step1State(
val selected: Variant ) data class Step2State( val option1Checked: Boolean, val option2Checked: Boolean ) -
enum class Variant { Variant1, Variant2 } data class Step1State(
val selected: Variant ) data class Step2State( val option1Checked: Boolean, val option2Checked: Boolean ) data class Step3State( val name : String, val description : String ) -
data class WizardState( val currentScreenIndex: Int, val step1State: Step1State, val
step2State: Step2State, val step3State: Step3State ) -
Refactoring 1. Define States 2. MVP: replace all view.showX() →
view.render(state) MVVM: replace all LiveData<X> → LiveData<State> 3. Define a state machine with a clear API Inputs and Outputs 4. Define Intents that trigger inputs on state machine 5. Trigger Intents from view layer -
Links - The State of Representing State by Christina Lee
- RxJava By Example - Vol 3. the Multicast Edition by Kaushik Gopal - Screenshot testing: https://facebook.github.io/screenshot-tests-for-android/ - https://github.com/freeletics/RxRedux