Recipes in RxJava for Android

6f45238394f11243126f8719a5147fc1?s=47 Sasa Sekulic
December 05, 2015

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

6f45238394f11243126f8719a5147fc1?s=128

Sasa Sekulic

December 05, 2015
Tweet

Transcript

  1. 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
  2. 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
  3. #BTJDTPG3Y+BWB  Observer  Observable  Subscription  Subscriber (Observer

    & Subscription)  Subject (Observer & Observable) 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD
  4. 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
  5. 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
  6. 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
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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
  12. 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
  13. 4VCKFDUTm NVTJDQMBZFSFYBNQMF69 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD

  14. 4VCKFDUTm NVTJDQMBZFSFYBNQMFTUBUFT 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD

  15. 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
  16. 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
  17. 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
  18. 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
  19. 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
  20. $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
  21. $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
  22. 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
  23. 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
  24. 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
  25. .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
  26. .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
  27. .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
  28. .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
  29. .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
  30. .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
  31. .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
  32. 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
  33. %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
  34. %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
  35. '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