talk to RxJava ▸ A collection of thoughts around architectural experiments that went both right and wrong ▸ Based on ideas from other platforms and architectures ▸ The knowledge gained from applying them on a live app with hundreds of thousands of Daily Active Users ▸ Not a complete or canonical implementation, but more like reference examples and tips to come back to ▸ Do not get lost in the code snippets now ▸ Longer SpeakerDeck, links, and recap at the end!
retains a presenter ▸ Bind and unbind presenter ▸ Destroy presenter reference just in case public class MyView implements MVPView { Presenter p = null; public MyView() { presenter = new Presenter(); } public void onViewCreated() { presenter.bind(this); } public void onDestroy() { presenter.unbind(); presenter = null; } … }
▸ No fields are retained, everything is a parameter ▸ No return needed, just binds and handles itself void start(View v, Observable<MyLifecycle> lifecycle, Scheduler main) { bind(lifecycle, main, view.listClicks().flatMap(getInfoForPosition()), view.setDetails()); bind(lifecycle, main, view.endOfList().flatMap(getMoreElements()), view.setElements()); } Values come from class or static. No judgement, just make this function pure.
lifecycle ▸ Rx-aware MVP libraries - Pre-built support ▸ Roll your own binding according to your app’s needs ▸ Views instead of Fragments? ▸ Legacy architecture?
exactly your needs ▸ Works with legacy code without disrupting BehaviorRelay<ActivityLifecycle> lifecycle = BehaviorRelay.create() @Override protected void onCreate(Bundle b) { if (b == null) { lifecycle.call(ENTER); } lifecycle.call(CREATE); } … @Override protected void onDestroy() { super.onDestroy(); lifecycle.call(DESTROY); if (isFinishing()) { lifecycle.call(EXIT); } } https://github.com/pakoito/FunctionalAndroidReference/blob/master/liblogic/src/main/kotlin/com/pacoworks/ dereference/architecture/reactive/buddies/ReactiveActivity.kt Doesn’t require inheriting from BaseActivity You can use a delegate class and compose
BehaviorRelay.create(); PublishRelay<Nothing> clicks = PublishRelay.create(); … state.flatMap(currentState -> clicks .first() .flatMap(element -> NetworkApi.requestInfo(element.id)) .map(changes -> currentState.applyDelta(changes) ).subscribe(state) 1. There exists a current state 2. For every value of current state 3. Wait for the first signal 4. Make any requests required to modify current data 5. Apply new data to old data 6. Set new data as current state 7. Wait for the first signal…
with an initial value, followed by a sequence of immutable values over time Every value is a snapshot of the state, and it can be trusted until the next value is received. State is processed sequentially
with an initial value, followed by a sequence of immutable values over time ▸ State has to be explicit and measurable data Every value is a snapshot of the state, and it can be trusted until the next value is received. State is processed sequentially
with an initial value, followed by a sequence of immutable values over time ▸ State has to be explicit and measurable data ▸ The state has to be external to the use case, and is passed as a dependency into it Every value is a snapshot of the state, and it can be trusted until the next value is received. State is processed sequentially
with an initial value, followed by a sequence of immutable values over time ▸ State has to be explicit and measurable data ▸ The state has to be external to the use case, and is passed as a dependency into it ▸ Can be implemented using a Behaviour subject, or a Serialised one if state has be thread-safe Every value is a snapshot of the state, and it can be trusted until the next value is received. State is processed sequentially
active: RxBinding, RxPaper, RxPaparazzo, RxFileObserver… https://github.com/zsoltk/RxAndroidLibs ▸ Build for your own components: ItemTouchHelper, Exoplayer, ViewsViewPager, RecyclerAdapter… ▸ And release them as open-source!
Create the state with an initial value */ StubView view = new StubView(); Pair startState = Pair.with(createHome(), Direction.FORWARD); BehaviorRelay navigation = createStateHolder(startState); TestSubscriber testSubscriber = TestSubscriber.create<Pair<Screen, Direction>>(); navigation.value.subscribe(testSubscriber); /* Start the subscription */ subscribeHomeInteractor(view, navigation); /* Act on screen by forwarding a value */ Pair newState = Pair.with(createRotation(), Direction.FORWARD); view.screenClick.call(newState); /* Assert correct output of a sequence of values */ testSubscriber.assertValueCount(2); testSubscriber.assertValues(startState, newState); testSubscriber.assertNoTerminalEvent(); }
null; void onCreate() { /** * Create an observable which emits on {@code view} click events. * The emitted value is unspecified and should only be used as * notification. * * The created observable keeps a strong reference to {@code view}. * Unsubscribe to free this reference. * * The created observable uses {@link View#setOnClickListener} to * observe clicks. * Only one observable can be used for a view at a time. */ viewClicks = RxView.clicks(view); } Observable<Element> viewClicks() { return viewClicks; } VIEW RXVIEW USE CASE
BehaviorRelay.create(); PublishRelay<Nothing> clicks = PublishRelay.create(); … state.flatMap(currentState -> clicks .first() .flatMap(element -> NetworkApi.requestInfo(element.id)) .map(changes -> currentState.applyDelta(changes) ).subscribe(state) ‣ Pure input-output cases ‣ No strong references to the view ‣ No main thread requirement ‣ No out-of-domain class dependencies?
PublishRelay<Nothing> clicks = PublishRelay.create(); /* Real implementation for integration */ Func1<String, Observable<Info>> networkRequest = { id -> NetworkService.requestInfo(id); } /* Mock implementation for errors */ Func1<String, Observable<Info>> failRequest = { id -> Observable.error(); } … state.flatMap(currentState -> clicks .first() .flatMap(element -> networkRequest.call(element.id)) .map(changes -> currentState.applyDelta(changes) ).subscribe(state) ‣ Replace dependencies on interfaces and statics with just the functions required for each use case Easy replacement for server responses, sensors, or events like lifecycle without needing a framework for testing Implementation is still coupled and checked by the compiler and tests
= PublishRelay.create(); Func1<String, Observable<Info>> networkRequest = { id -> NetworkService.requestInfo(id); } … doFM( { () -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.applyDelta(changes) }) ).subscribe(state) ‣ Abstraction over nested flatMap, switchMap, and concatMap ‣ Improves readability ‣ Helps noticing subtle errors like using toList() on non-finite observables https://github.com/pakoito/ RxComprehensions doFM(), doSM(), and doCM() Every nested depth receives the result of all Observables above it Uses a function returning an Observable for each depth of nesting: Func0, Func1, Func2… Structurally, it splits use cases into reusable functions with N == depth parameters and 1 output
BehaviorRelay.create(); PublishRelay<Nothing> clicks = PublishRelay.create(); Func1<String, Observable<Info>> networkRequest = { id -> NetworkService.requestInfo(id); } … doFM( { () -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.applyDelta(changes) }) ).subscribe(state) ‣ Pure input-output cases ‣ No strong references to the view ‣ No main thread requirement ‣ No out-of-domain class dependencies ‣ Nested flatMap, switchMap, or concatMap ‣ Most use cases follow the same pattern
() -> state }, /* A terminating Observable */ { current -> clicks.first() }, /* Maybe more terminating Observables */ { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.delta(changes) }) ).subscribe(state) Use cases follow the same pattern: ‣ A non-terminating Observable ‣ flatMap/switchMap/concatMap ‣ A terminating Observable ‣ flatMap/switchMap/concatMap ‣ A terminating Observable ‣ … repeat 0 - N times ‣ Subscription to the new state No Exceptions! * *Some exceptions
yet: ‣ Use onRetainCustomNonConfigurationInstance() if you don’t care about losing state if your app is killed by the system ‣ Store in a static and handle its lifecycle manually ‣ Store in the Application class ‣ Save each state individually on the bundle ‣ Use a Dependency Injection framework ‣ Make all state go through persistence We’re open to new ideas!
in the system. Can be caused by framework problems, environment, mistakes in the code… ▸ An error is an expected problem that you provision for. Can be caused by business and framework requirements. ▸ You want defects to be reported, and errors to be handled. You have to model errors into your domain.
() -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.delta(changes) }) ).subscribe(state, toastError) void onResume() { clicks.onError(new RuntimeException()); } ‣ WTF ‣ This shouldn’t happen ‣ I wanted a stack trace but all I got is a lousy Toast
() -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.delta(changes) }) ).subscribe(state, toastError) void onResume() { clicks.onError(new RuntimeException()); } ‣ WTF ‣ This shouldn’t happen ‣ I wanted a stack trace but all I got is a lousy Toast ‣ Non-terminating Observable is unsubscribed
() -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.delta(changes) }) ).subscribe(state, toastError) void onResume() { clicks.onError(new RuntimeException()); } ‣ WTF ‣ This shouldn’t happen ‣ I wanted a stack trace but all I got is a lousy Toast ‣ Non-terminating Observable is unsubscribed ‣ The app isn’t working correctly, and the user doesn’t know why
or every architecture. Do it responsibly! ▸ Helps catching errors in development stages rather than in live ▸ Doesn’t leave the app in an inconsistent state for the user. An unresponsive app might be worse than a crash ▸ No silent longstanding failures becoming bad user reviews ▸ Pick up the crash log and fix it for next release!
▸ Debug with subscribe(Action1, Action1) ▸ Unlike Observer, Actions compose naturally. You can build your own tools around them: logging, threading, tracing… ▸ Subjects are not Action1. Use RxRelay instead ▸ All samples in this presentation used Relays! https://github.com/JakeWharton/RxRelay
{ current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> currentState.delta(changes) }) ).subscribe(state) ‣ Exist inside the system ‣ Every IO operation ‣ Every network call ‣ Every database query ‣ Every interaction with the Android framework ‣ Every reactive library ‣ Everything outside your use case island that you don’t know about
state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .doOnError(logError()) .onErrorResumeNext( Observable.empty()) .map(changes -> current.delta(changes) }) ).subscribe(state) ‣ Very basic ‣ Removes the problem but not the cause ‣ You need to forward your logs to a report system ‣ Not recommended
{ current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .map(changes -> current.delta(changes) }) ).retry(/* */) .subscribe(state) ‣ Delays the problem instead of solving it ‣ Problems with inconsistency and persistence
= BehaviorRelay.create(Transaction.Idle); doFM( { () -> state }, { current -> clicks.first() }, { current, clicks -> networkRequest.call(element.id) .onErrorResumeNext( Observable.just( Transaction.Fail( “User Offline”))) .map(changes -> Transaction.Success( current.delta(changes)) }) ).subscribe(state) ‣ Your single state becomes a state machine ‣ The states already exist, we’re just making them explicit as data! Testing done by asserting for every possible input/output of the state machine All states have to be handled by a use case
object Idle: Transaction() class Loading(val percent: Int): Transaction() class Fail(val cause: String): Transaction() class Success(val info: Info): Transaction() } val state: Transaction = Loading(10) when (state) { is Idle -> /* */ is Loading -> /* */ is Fail -> /* */ is Success -> /* */ } ‣ Sealed classes are closed inheritances ‣ Like enums but every element can be a different class ‣ Your code defines the transitions between states ‣ Matched with function when() ‣ The compiler checks that you always handle all possible cases http://tinyurl.com/KMobi16
public class Loading { public final int percent; … } public class Fail { public final String cause; … } public class Success { public final Info info; … } Union4.Factory<Idle, Loading, Fail, Success> FACTORY = GenericUnions.quartetFactory(); Union4<Idle, Loading, Fail, Success> state = FACTORY.second(new Loading(50)); state.join({ idle -> /* */ }, { loading -> /* */ }, { fail -> /* */ }, { success -> /* */ }) ‣ Generic encoding of unions, similar to Scala ‣ Single, Optional, Either, Union3-9 ‣ Matched with methods join() and continued() ‣ More verbose than Kotlin’s ‣ The compiler still checks that you always handle all possible cases https://github.com/pakoito/ RxSealedUnions
your problems into small use cases ▸ Make each use case a data input-output problem ▸ Model your solutions with data, not code ▸ The lifecycle and errors are data too! ▸ State has to be explicit and debuggable at any point ▸ Handle both defects and errors responsibly
available on the droidcon London website at the following link: https://uk.droidcon.com/#skillscasts Sample app https://github.com/pakoito/ FunctionalAndroidReference Modeling state with Sealed Classes tinyurl.com/KMobi16 Rx libraries https://github.com/zsoltk/RxAndroidLibs RxRelay https://github.com/JakeWharton/RxRelay RxComprehensions https://github.com/pakoito/ RxComprehensions RxSealedUnions https://github.com/pakoito/RxSealedUnions More functional goodies https://github.com/pakoito/FunctionalRx