Going Reactive An architectural journey Going Reactive An - - PowerPoint PPT Presentation
Going Reactive An architectural journey Going Reactive An - - PowerPoint PPT Presentation
Going Reactive An architectural journey Going Reactive An architectural journey Matthias Kppler October 2015 commit 24c61b35754ff5ca153ce37c5886279153f0d16f Author: Matthias Kaeppler <mk@soundcloud.com> Date: Wed Mar 13
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
Rx
Screen Presenter
Life-cycle dispatch, view binding
Feature Operations
Business logic, data wiring, scheduling
Storage & API
Network, database, flat files, syncer Rx[Android]
Views
class SoundStreamFragment extends LightCycleSupportFragment { @Inject @LightCycle SoundStreamPresenter presenter; public SoundStreamFragment() { setRetainInstance(true); ... } ... }
LightCycle
A C B
- nCreate
LightCycle Dispatcher
@LightCycle @LightCycle @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
p1 p2 p3
*current *next
p1
- nNext(p1)
subscribe() / next()
PublishSubject
switchOnNext PagingFunction
Pager
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
@Override public void call(PropertySet changeSet) { eventBus.publish( EventQueue.ENTITY_STATE_CHANGED, EntityStateChangedEvent.fromLike(changeSet) ); }
publishChangeSet: Action1<PropertySet>
RxSubject in disguise!
Screen-to-Screen Updates
protected void onViewCreated(...) { eventBus.subscribe( EventQueue.ENTITY_STATE_CHANGED, new UpdateListSubscriber(adapter) ); }
SoundStreamPresenter
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()) } }
Observable<Integer> intSequence() { return Observable.create((subscriber) -> { List<Integer> ints = computeListOfInts(); for (int n : ints) { subscriber.onNext(n); subscriber.onCompleted(); } }
Deferred Execution
Observable<Integer> intSequence() { return Observable.defer(() -> { return Observable.from(computeListOfInts()); } }
expensive!
Common Pitfalls
No-args subscribe
Observable.create(...).subscribe(/* no-args */)
OnErrorNotImplementedException
ObserveOn: onError
Observable.create((subscriber) -> { subscriber.onNext(value); subscriber.onError(new Exception()); }.observeOn(mainThread()).subscribe(...)
- nError cuts ahead of onNext
gets dropped!
ObserveOn: Backpressure
public void onStart() { request(RxRingBuffer.SIZE); } public void onNext(final T t) { ... if (!queue.offer(on.next(t))) {
- nError(new MissingBackpressureException());
return; } schedule(); }
16!
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()]
.
soundcloud.com Berlin New York San Francisco London