Going Reactive An architectural journey
Going Reactive An architectural journey Matthias Käppler October 2015
commit 24c61b35754ff5ca153ce37c5886279153f0d16f Author: Matthias Kaeppler <mk@soundcloud.com> Date: Wed Mar 13 16:09:04 2013 +0100 Throw RxJava into the mix! diff --git a/app/pom.xml b/app/pom.xml index 86ba988..1bf5109 100644 --- a/app/pom.xml +++ b/app/pom.xml @@ -178,6 +178,11 @@ + <dependency> + <groupId>com.netflix.rxjava</groupId> + <artifactId>rxjava-core</artifactId> + <version>0.5.4</version> + </dependency> </dependencies>
Journey Down The Stack
Layered Architecture
Featurized Architecture
Featurized Layers
The Sound Stream
Layer Objects Screen Presenter Life-cycle dispatch, view binding Rx[Android] Feature Operations Business logic, data wiring, scheduling Rx Storage & API Network, database, flat files, syncer
Views class SoundStreamFragment extends LightCycleSupportFragment { @Inject @LightCycle SoundStreamPresenter presenter; public SoundStreamFragment() { setRetainInstance(true); ... } ... }
LightCycle A @LightCycle onCreate LightCycle B @LightCycle Dispatcher C @LightCycle
Presenters class SoundStreamPresenter extends RecyclerViewPresenter<StreamItem> { ... @Override protected CollectionBinding<StreamItem> onBuildBinding(Bundle args) { return CollectionBinding.from( streamOperations.initialStreamItems()) .withAdapter(adapter) .withPager(streamOperations.pagingFunction()) .build(); } }
Paging
Paging onNext(p1) Pager subscribe() switchOnNext / next() PublishSubject PagingFunction p1 *next *current p3 p2 p1
Use Cases class SoundStreamOperations { Observable<List<StreamItem>> initialStreamItems() { return loadFirstPageOfStream() .zipWith( facebookInvites.loadWithPictures(), prependFacebookInvites()) .subscribeOn(scheduler); } ... }
Feature Data class SoundStreamStorage { Observable<PropertySet> streamItems(int limit) { Query query = Query.from(“SoundStreamTable”).limit(limit); return propellerRx.query(query).map(new StreamItemMapper()); } ... }
Cross-Feature Communication
Cross-Screen Messaging updated!
Screen-to-Screen Updates Rx Subject
Screen-to-Screen Updates Observable<PropertySet> toggleLike(Urn urn, boolean addLike) { return storeLikeCommand.toObservable(urn, addLike) .map(toChangeSet(targetUrn, addLike)) .doOnNext(publishChangeSet); }
Screen-to-Screen Updates publishChangeSet: Action1<PropertySet> @Override public void call(PropertySet changeSet) { eventBus.publish( RxSubject in disguise! EventQueue.ENTITY_STATE_CHANGED, EntityStateChangedEvent.fromLike(changeSet) ); }
Screen-to-Screen Updates SoundStreamPresenter protected void onViewCreated(...) { eventBus.subscribe( EventQueue.ENTITY_STATE_CHANGED, new UpdateListSubscriber(adapter) ); }
Implementation Patterns
Life-Cycle Subscriptions private CompositeSubscription viewLifeCycle; protected void onViewCreated(...) { viewLifeCycle = new CompositeSubscription(); viewLifeCycle.add(...); ... } protected void onDestroyView() { viewLifeCycle.unsubscribe(); }
Fast Path & Lazy Updates Observable<Model> maybeCached() { return Observable.concat(cachedModel(), remoteModel()).first() }
Observable Transformers Observable<Model> scheduledModel() { return Observable.create(...).compose(schedulingStrategy) } class HighPrioUiTask<T> extends Transformer<T, T> { public Observable<T> call(Observable<T> source) { return source .subscribeOn(Schedulers.HIGH_PRIO) .observeOn(AndroidSchedulers.mainThread()) } }
Deferred Execution Observable<Integer> intSequence() { return Observable.create((subscriber) -> { List<Integer> ints = computeListOfInts(); for (int n : ints) { expensive! subscriber.onNext(n); subscriber.onCompleted(); } } Observable<Integer> intSequence() { return Observable.defer(() -> { return Observable.from(computeListOfInts()); } }
Common Pitfalls
No-args subscribe Observable.create(...).subscribe(/* no-args */) OnErrorNotImplementedException
ObserveOn: onError gets dropped! Observable.create((subscriber) -> { subscriber.onNext(value); subscriber.onError(new Exception()); }.observeOn(mainThread()).subscribe(...) onError cuts ahead of onNext
ObserveOn: Backpressure 16! public void onStart() { request(RxRingBuffer.SIZE); } public void onNext(final T t) { ... if (!queue.offer(on.next(t))) { onError(new MissingBackpressureException()); return; } schedule(); }
ObserveOn: Backpressure ★ Take load off of target thread˝ ★ Use buffering operators (buffer, toList, …)˝ ★ Use onBackpressure* operators˝ ★ System.setProperty(“rx.ring-buffer.size”)
Debugging
Debugging Observables Observable.just(1, 2, 3) .map((n) -> {return Integer.toString(n);} .observeOn(AndroidSchedulers.mainThread()); How do we debug this?
Gandalf ★ Annotation based byte code injection ★ Based on Hugo https://github.com/JakeWharton/hugo ★ AspectJ + Gradle plugin ★ @RxLogObservable, @RxLogSubscriber
Gandalf @RxLogObservable Observable<String> createObservable() { return Observable.just(1, 2, 3) .map((n) -> {return Integer.toString(n);} .observeOn(mainThread()); } @RxLogSubscriber class StringSubscriber extends Subscriber<String> {}
Gandalf [@Observable :: @InClass -> MainActivity :: @Method -> createObservable()] [@Observable#createObservable -> onSubscribe() :: @SubscribeOn -> main] [@Observable#createObservable -> onNext() -> 1] [@Observable#createObservable -> onNext() -> 2] [@Observable#createObservable -> onNext() -> 3] [@Observable#createObservable -> onCompleted()] [@Observable#createObservable -> onTerminate() :: @Emitted -> 3 elements :: @Time -> 4 ms] [@Observable#createObservable -> onUnsubscribe()]
Stay in touch @mttkay . soundcloud.com Berlin New York San Francisco London
Recommend
More recommend