Share
Embed
Recipes in RxJava for Android
Some real-life Rx recipes for Android - how to create Observables, when to use Subjects, how to control the observer lifecycle with Subscriptions, how to control multithreading etc. Presented at Droidcon Krakow (Poland) in December 2015.
View video here: https://www.youtube.com/watch?v=JrWTMqd_UUo
Sasa Sekulic
December 05, 2015
Transcript
35 slides
-
3FDJQFTJO3Y+BWB GPS "OESPJE Sasa Sekulic co-author of the upcoming Manning
book “Grokking Rx”(with Fabrizio Chignoli and Ivan Morgillo); developer of the UN-WFP ShareTheMeal app; cofounder of Alter Ego Solutions @sasa_sekulic | www.alterego.solutions -
6/8PSME'PPE1SPHSBNNF 4IBSF5IF.FBM We provide food to schoolchildren in need
– over 3.000.000 meals shared! It costs only €0.40 to feed one child for a day. www.sharethemeal.org Google Play Best of 2015, Google Play Best of Local Apps 2015 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
#BTJDTPG3Y+BWB Observer Observable Subscription Subscriber (Observer
& Subscription) Subject (Observer & Observable) 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
0CTFSWBCMFDSFBUJPO just(), from() – simple, executed immediately upon creation
create() – executed upon subscription but need to take care of contract calls defer() – simple, but executed upon subscription 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
0CTFSWBCMFDSFBUJPOmFYBNQMFUPPTJNQMF just(), from() – simple, executed immediately upon creation
public Observable<Boolean> exampleBlocking() { SharedPreferences sharedPreferences = context.getSharedPreferences("prefs", Context.MODE_PRIVATE); return Observable.just(sharedPreferences.getBoolean("boolean", false)); } create() – executed upon subscription but need to take care of contract calls defer() – simple, but executed upon subscription 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
0CTFSWBCMFDSFBUJPOmFYBNQMFUPPDPNQMJDBUFE just(), from() – simple, executed immediately upon creation
create() – executed upon subscription but need to take care of contract calls public Observable<Boolean> exampleTooComplicated() { SharedPreferences sharedPreferences = context.getSharedPreferences("prefs", Context.MODE_PRIVATE); return Observable.create(new Observable.OnSubscribe<Boolean>() { @Override public void call(Subscriber<? super Boolean> subscriber) { if (subscriber.isUnsubscribed()) { return; } subscriber.onNext(sharedPreferences.getBoolean("boolean", false)); if (!subscriber.isUnsubscribed()) { subscriber.onCompleted(); } } }); } defer() – simple, but executed upon subscription 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
0CTFSWBCMFDSFBUJPOmDSFBUF JTOUBTFBTZBTJU MPPLT think about the unsubscription/backpressure chain Observable.create(s
-> { int i = 0; while (true) { s.onNext(i++); } }).subscribe(System.out::println); for list of many more other problems: http://akarnokd.blogspot.hu/2015/05/pitfalls-of-operator- implementations.html 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
0CTFSWBCMFDSFBUJPOmFYBNQMFKVTUSJHIU just(), from() – simple, executed immediately upon creation
create() – executed upon subscription but need to take care of contract calls defer() – simple, but executed upon subscription public Observable<Boolean> exampleJustRight() { SharedPreferences sharedPreferences = context.getSharedPreferences("prefs", Context.MODE_PRIVATE); return Observable.defer(new Func0<Observable<Boolean>>() { @Override public Observable<Boolean> call() { return Observable.just(sharedPreferences.getBoolean("boolean", false)); } }); } 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
0CTFSWBCMFDSFBUJPOmTQFDJBMDBTFPGGSPN from(Future<? extends T>) – blocking, cannot unsubscribe (but
you can specify timeout or Scheduler) from(Iterable<? extends T>), from(T[]) – convert Iterable/array event into events from Iterable/array List<Boolean> list = new ArrayList<>(); Observable.just(list).subscribe(new Observer<List<Boolean>>(){ ... } //takes List<Boolean>, emits List<Boolean> Observable.from(list).subscribe(new Observer<Boolean>(){ ... } //takes List<Boolean>, emits Boolean items from the list 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
0CTFSWBCMFDSFBUJPOmGPS&BDI FRVJWBMFOUPQT List<Boolean> list = new ArrayList<>(); Observable.from(list) .map(new Func1<Boolean,
Boolean>() { @Override public Boolean call(Boolean aBoolean) { return !aBoolean; //returns value } }) .flatMap(new Func1<Boolean, Observable<Integer>>() { @Override public Observable<Integer> call(Boolean aBoolean) { return Observable.just(aBoolean.hashCode()); //returns observable<value> } }) .toList() //or .toSortedList() .subscribe(new Observer<List<Integer>>() { 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
4VCKFDUT use them to send values to Observers outside
creation block enable you to have communication between the Observer and Observable- value generator access onNext(), onError() and onCompleted() whenever you want thread-safe, but don't mix threads they're Hot observables – the values are generated independently of existence of subscribers 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
4VCKFDUT UZQFT PublishSubject - doesn’t store any states, just
sends what it receives while subscribed (you miss anything in between); epitome of Hot observable BehaviourSubject – remembers the last value; emptied after onCompleted()/onError(); can be initialized with a value AsyncSubject – remembers the last value; after onCompleted() subscribers receive the last value and the onCompleted() event; after onError() subscribers receive just the onError() event ReplaySubject – remembers everything but can be limited (create(int bufferCapacity), createWithSize(int size), createWithTime(long time, TimeUnit unit, final Scheduler scheduler), createWithTimeAndSize(long time, TimeUnit unit, int size, final Scheduler scheduler)) 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
4VCKFDUTm NVTJDQMBZFSFYBNQMF0CTFSWFS public class MusicPlayer { public enum PLAYER_STATE {STOPPED,
PLAYING} Observer<PLAYER_STATE> playerObserver = new Observer<PLAYER_STATE>() { @Override public void onNext(PLAYER_STATE state) { currentPlayerState = state; if (state == PLAYER_STATE.PLAYING) { startSong(); showPauseButton(); } else if (state == PLAYER_STATE.STOPPED) { stopSong(); showPlayButton(); } } @Override public void onError(Throwable e) {} @Override public void onCompleted() {} }; } 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
4VCKFDUTm NVTJDQMBZFSFYBNQMF4VCKFDU public class MusicPlayer { PLAYER_STATE currentPlayerState = PLAYER_STATE.STOPPED;
BehaviorSubject<PLAYER_STATE> playerStateSubject = BehaviorSubject.create(currentPlayerState); public MusicPlayer() { playerStateSubject.subscribe(playerObserver); } private void pressButton() { if (currentPlayerState == PLAYER_STATE.PLAYING) { playerStateSubject.onNext(PLAYER_STATE.STOPPED); } else if (currentPlayerState == PLAYER_STATE.STOPPED) { playerStateSubject.onNext(PLAYER_STATE.PLAYING); } } } 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
4VCKFDUTm NVTJDQMBZFSFYBNQMFPQUJNJ[BUJPO We don't need currentPlayerState, just use playerStateSubject
.getValue() Don't expose Subject to others: when offering them for subscribing, use getObservable() public Observable<PLAYER_STATE> getPlayerStateObservable() { return playerStateSubject.asObservable(); } Use Subscription to control Observer lifecycle! 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
4VCTDSJQUJPO Subscription is a relationship between the Observer and
the Observable public MusicPlayer() { Subscription stateSub = playerStateSubject.subscribe(playerObserver); } Subscription is returned by the subscribe() public MusicPlayer() { Subscription stateSub = playerStateSubject //Observable .filter(state -> state == PLAYING) //Observable .map(state -> someMethod()) //Observable .subscribe(playerObserver); //Subscription } Very simple: isUnsubscribed(), unsubscribe() 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
4VCTDSJQUJPO VTBHF To control the lifecycle and/or release the
resources, call it in onPause()/onStop()/onDestroy(): @Override protected void onDestroy() { super.onDestroy(); if (mPlayerSubscription != null && !mPlayerSubscription.isUnsubscribed()) { mPlayerSubscription.unsubscribe(); } } To prevent multiple executions/subscriptions: public MusicPlayer() { if (mPlayerSubscription == null || mPlayerSubscription.isUnsubscribed()) { mPlayerSubscription = playerStateSubject.subscribe(playerObserver); } } If you don't want to do null checks, initialize the subscription: Subscription mPlayerSubscription = Subscriptions.empty(); //or unsubscribed(); Important: unsubscribing has no impact on the observable or other subscriptions! 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
$PNQPTJUF4VCTDSJQUJPO m HSPVQJOH4VCTDSJQUJPOT When you need to control multiple subscriptions
at the same time – adding with constructor or add(): CompositeSubscription activitySubscriptions = new CompositeSubscription(); @Override protected void onCreate() { mPlayerSub = playerStateSubject.subscribe(playerObserver); mPlayerSub2 = playerStateSubject.subscribe(playerObserver2); activitySubscriptions = new CompositeSubscriptions(mPlayerSub, mPlayerSub2); } @Override protected void onResume() { mPlayerSub3 = playerStateSubject.subscribe(playerObserver3); activitySubscriptions.add(mPlayerSub3); } 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
$PNQPTJUF4VCTDSJQUJPO m HSPVQJOH4VCTDSJQUJPOT DPOUE When you need to control multiple
subscriptions at the same time: removing with remove(), unsubscribing all with unsubscribe(), and clear() unsubscribes all and removes them from the CompositeSubscription: @Override protected void onPause() { mPlayerSub3 = playerStateSubject.subscribe(playerObserver3); activitySubscriptions.remove(mPlayerSub3); } @Override protected void onDestroy() { super.onDestroy(); if (activitySubscriptions.hasSubscriptions() && !activitySubscriptions.isUnsubscribed()) { activitySubscriptions.unsubscribe(); } } 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
4VCTDSJQUJPOmDPOUSPMUIFMJGFDZDMF Manually, on the Activity or Fragment level –
create when you want, unsubscribe in onPause()/onStop()/onDestroy() Automatically, on the Activity or Fragment level – use https://github.com/trello/RxLifecycle public class MainActivity extends RxAppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Observable.interval(1, TimeUnit.SECONDS) .compose(this.<Long>bindUntilEvent(ActivityEvent.PAUSE)) //binds to onPause() .subscribe(onCreateObserver); } } .compose(this.<Long>bindUntilEvent(ActivityEvent.DESTROY)) //binds to onDestroy() ... .compose(this.<Long>bindToLifecycle()) //binds automatically to opposite method (if called from onResume() it will be bound to onPause() One very important note: RxLifecycle doesn't call unsubscribe() but calls onCompleted() instead! 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
4VCTDSJQUJPOm DPOUSPMUIFMJGFDZDMF HMPCBM Manually on the global, Application level
– create when you want, and unsubscribe? Never, so do the transactions atomically, close the observables and unsubscribe from activities/fragments: public class SplashActivity extends AppCompatActivity { @Inject UserManager mUserManager; //using Dagger! Subscription mUserSubscription = Subscriptions.empty(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mUserManager.getUserObservable() .subscribe(user -> continue(), throwable -> showError(throwable)); } @Override protected void onDestroy() { super.onDestroy(); if (!mUserSubscription.isUnsubscribed()) mUserSubscription.unsubscribe(); } } 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
4VCTDSJQUJPOm DPOUSPMUIFMJGFDZDMF HMPCBM DPOUE public UserManager () { //from() will
automatically call onCompleted() so no pending references Observable.from(loadUserFromSavedPreferences()) .subscribe(user -> mUserSubject.onNext(user), throwable -> mUserSubject.onError(throwable)); } Observable<User> getUserObservable() { return mUserSubject.asObservable(); } } There's no automated way to manage lifecycle on the global level! 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
.BOBHFSFYBNQMF CPVOEUP"QQDPOUFYU loads a configuration, then loads a saved user,
and propagates it: public UserManager () { public UserManager() { mConfigurationManager.getCurrentConfigObservable() .flatMap(config -> loadUserFromSavedPreferences()) .subscribe(user -> mUserSubject.onNext(user), throwable -> mUserSubject.onError(throwable)); } Observable<User> getUserObservable() { return mUserSubject.asObservable(); } } 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
.BOBHFSFYBNQMF FSSPSIBOEMJOH sending onError() closes the subscription and no
more events are sent! onError() handles ALL errors in the chain! to prevent this, use error handling methods: – onErrorReturn() - use other value – onErrorResumeNext() - use other observable – onExceptionResumeNext() - handles Exceptions but not Throwables public UserManager () { public UserManager() { mConfigurationManager.getCurrentConfigObservable() .onErrorReturn(config -> Config.Empty) .flatMap(config -> loadUserFromSavedPreferences()) .onErrorReturn(user -> User.Empty) .subscribe(user -> mUserSubject.onNext(user); } Observable<User> getUserObservable() { return mUserSubject.asObservable(); } } 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
.BOBHFSFYBNQMF TQMJUUJOHTVCTDSJQUJPOT the whole of the chain is an
Observable until the subscribe() if you don’t use error management (onErrorReturn()/onErrorResumeNext()…), all subscriptions have to have onError() public UserManager() { Observable<Config) configObservable = mConfigurationManager.getCurrentConfigObservable() .onErrorReturn(config -> Config.Empty); Subscription configValidSub = configObservable .filter(config -> config != null && config != Config.Empty) .flatMap(config -> loadUserFromSavedPreferences()) .onErrorReturn(user -> User.Empty) .subscribe(user -> mUserSubject.onNext(user)); Subscription configInvalidSub = configObservable .filter(config -> config == null || config == Config.Empty) .subscribe(config -> Log.d("App", "config empty!"); } 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
.BOBHFSFYBNQMF SFVTJOHPCTFSWBCMFDIBJOT if you have a part of the
chain that repeats itself – extract it in a Transformer<T, R> () Subscription configValidUserValidSub = configObservable .filter(config -> config != null && config != Config.Empty) .flatMap(config -> loadUserFromSavedPreferences()) .onErrorReturn(user -> User.Empty) .filter(user -> user != User.Empty) .subscribe(user -> mUserSubject.onNext(user)); Subscription configValidUserInalidSub = configObservable .filter(config -> config != null && config != Config.Empty) .flatMap(config -> loadUserFromSavedPreferences()) .onErrorReturn(user -> User.Empty) .filter(user -> user == User.Empty) .subscribe(user -> Log.d("APP", "user empty!"); 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
.BOBHFSFYBNQMF SFVTJOHPCTFSWBCMFDIBJOT DPOUE Observable.Transformer<Config, User> convertConfigToUser() { return new Observable.Transformer<Config,
User>() { @Override public Observable<User> call(Observable<Config> configObservable) { return configObservable .filter(config -> config != null && config != Config.Empty) .flatMap(config -> loadUserFromSavedPreferences()) .onErrorReturn(user -> User.Empty); } }; } Subscription configValidUserValidSub = configObservable .compose(convertConfigToUser()) .filter(user -> user != User.Empty) .subscribe(user -> mUserSubject.onNext(user)); Subscription configValidUserInalidSub = configObservable .compose(convertConfigToUser()) .filter(user -> user == User.Empty) .subscribe(user -> Log.d("APP", "user empty!"); 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
.BOBHFSFYBNQMF NVMUJUISFBEJOH RxJava is asynchronous by default, but not
multithreaded by default! use explicit methods to change the thread: subscribeOn() – it changes the upstream thread (until that point) observeOn() – it changes the downstream thread (after that point) Observable.Transformer<Config, User> convertConfigToUser() { return new Observable.Transformer<Config, User>() { @Override public Observable<User> call(Observable<Config> configObservable) { return configObservable .filter(config -> config != null && config != Config.Empty) .flatMap(config -> loadUserFromSavedPreferences()) .onErrorReturn(user -> User.Empty) .subscribeOn(Schedulers.io()) //everything up to this uses io() thread .observeOn(AndroidSchedulers.mainThread()); // everything after this // uses UI thread } }; } 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
.BOBHFSFYBNQMF NVMUJUISFBEJOH DPOUE RxJava Scheduler: computation() – bound
(limited by CPU cores) io() - unbound immediate() – executes immediately on current thread trampoline() – executes after finishing, on current thread newThread() – creates new thread from(Executor) – use custom executor test() – uses TestScheduler with manual scheduling controls rxandroid library: AndroidScheduler.mainThread() Multithreading is hard, RxJava makes it easier – but it's still not easy! do not specify the thread unless you really need to 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
3Y+BWB TJEFFGGFDUT Side effects: executed upon chain events, but are
not ending the chain and the execution is not a part of the chain Use when you need to perform additional actions whose outcome doesn't modify the chain of events (logging, data persistence, showing/hiding the progress bar etc.) Catch-all: doOnEach() – takes either Observer<T> or Action1<Notification<? super T> Depending on type of event: doOnNext(), doOnError(), doOnCompleted() but also doOnSubscribe(), doOnUnsubscribe(), doOnRequest(), doOnTerminate() Observable.from(loadUserFromSavedPreferences()) .doOnEach(notification -> Log.d("USER", "user = " + notification.getValue())) .doOnNext(user -> saveUserAgain(user)) .doOnError(throwable -> Log.d("USER", "WE'RE ALL GONNA DIE!!1")) .subscribe(observer); 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
%FCVHHJOHm MPHHJOH3Y+BWB FWFOUT Use side effects: doOnEach(), doOnNext(), doOnError()
etc. – works on single stream Use RxJavaDebug plugin (https://github.com/ReactiveX/RxJavaDebug) – works globally RxJavaPlugins.getInstance().registerObservableExecutionHook(new DebugHook( new SimpleDebugNotificationListener() { public Object onNext(DebugNotification n) { App.L.debug("onNext on " + n); return super.onNext(n); } public SimpleContext start(DebugNotification n) { App.L.debug("start on " + n); return super.start(n); } public void complete(SimpleContext<?> context) { super.complete(context); App.L.debug("complete on " + context); } public void error(SimpleContext<?> context, Throwable e) { super.error(context, e); App.L.debug("error = " + e + ", on " + context); } })); Use Frodo RxJava Logging Library: http://fernandocejas.com/2015/11/05/debugging-rxjava-on-android/ 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
%FCVHHJOHm MPHHJOH3Y+BWB FWFOUT DPOUE RxJavaDebug plugin output: 12-02 23:21:53.772: D/APP(1934):
start on {"observer": "rx.Observable$28@8f3a885", "type": "Subscribe", "source": "rx.Observable@8f576da", "sourceFunc": "rx.Observable$2@156370b"} 12-02 23:21:53.774: D/APP(1934): start on {"observer": "rx.Observable$28@8f3a885", "type": "Request", "n": 9223372036854775807, "from": "rx.internal.operators.OperatorMerge@3fcf9e8"} 12-02 23:21:53.774: D/APP(1934): complete on {"ns_duration": 166666, "threadId": 1, "notification": {"observer": "rx.Observable$28@8f3a885", "type": "Request", "n": 9223372036854775807, "from": "rx.internal.operators.OperatorMerge@3fcf9e8"}} 12-02 23:21:54.241: D/APP(1934): start on {"observer": "rx.Observable$28@eb33381", "type": "OnNext", "value": "MyObject(mDescription=Test1 Description, mTitle=Test1)", "from": "rx.internal.operators.OperatorAsObservable@607968e", "to": "rx.internal.operators.OperatorAsObservable@607968e"} 12-02 23:21:54.241: D/APP(1934): Logging MyObject mDescription = Test1 Description, mTitle = Test1 12-02 23:21:54.241: D/APP(1934): complete on {"ns_duration": 130938, "threadId": 1, "notification": {"observer": "rx.Observable$28@eb33381", "type": "OnNext", "value": "MyObject(mDescription=Test1 Description, mTitle=Test1)", "from": "rx.internal.operators.OperatorAsObservable@607968e", "to": "rx.internal.operators.OperatorAsObservable@607968e"}} Write your own RxJava plugins: RxJavaObservableExecutionHook, RxJavaSchedulersHook, RxJavaErrorhandler (https://github.com/ReactiveX/RxJava/wiki/Plugins) 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD -
'VSUIFSSFBEJOHXBUDIJOH https://github.com/ReactiveX/RxJava RxJava Single discussion: https://github.com/ReactiveX/RxJava/issues/1594 RxJava
Subscriber discussion: https://github.com/ReactiveX/RxJava/issues/792 ReactiveX homepage: http://reactivex.io/ Interactive marble diagrams for operators: http://www.rxmarbles.com/ Ben Christensen's talks: https://speakerdeck.com/benjchristensen/ Unit testing: http://fedepaol.github.io/blog/2015/09/13/testing-rxjava-observables-subscriptions/ http://alexismas.com/blog/2015/05/20/unit-testing-rxjava/ David Karnok's Advanced RxJava blog: http://akarnokd.blogspot.hu/ 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD