Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

#BTJDTPG3Y+BWB  Observer  Observable  Subscription  Subscriber (Observer & Subscription)  Subject (Observer & Observable) 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

0CTFSWBCMFDSFBUJPOmFYBNQMFUPPTJNQMF  just(), from() – simple, executed immediately upon creation public Observable 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

Slide 6

Slide 6 text

0CTFSWBCMFDSFBUJPOmFYBNQMFUPPDPNQMJDBUFE  just(), from() – simple, executed immediately upon creation  create() – executed upon subscription but need to take care of contract calls public Observable exampleTooComplicated() { SharedPreferences sharedPreferences = context.getSharedPreferences("prefs", Context.MODE_PRIVATE); return Observable.create(new Observable.OnSubscribe() { @Override public void call(Subscriber 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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

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 exampleJustRight() { SharedPreferences sharedPreferences = context.getSharedPreferences("prefs", Context.MODE_PRIVATE); return Observable.defer(new Func0>() { @Override public Observable call() { return Observable.just(sharedPreferences.getBoolean("boolean", false)); } }); } 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD

Slide 9

Slide 9 text

0CTFSWBCMFDSFBUJPOmTQFDJBMDBTFPGGSPN  from(Future) – blocking, cannot unsubscribe (but you can specify timeout or Scheduler)  from(Iterable), from(T[]) – convert Iterable/array event into events from Iterable/array List list = new ArrayList<>(); Observable.just(list).subscribe(new Observer>(){ ... } //takes List, emits List Observable.from(list).subscribe(new Observer(){ ... } //takes List, emits Boolean items from the list 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD

Slide 10

Slide 10 text

0CTFSWBCMFDSFBUJPOmGPS&BDI FRVJWBMFOUPQT List list = new ArrayList<>(); Observable.from(list) .map(new Func1() { @Override public Boolean call(Boolean aBoolean) { return !aBoolean; //returns value } }) .flatMap(new Func1>() { @Override public Observable call(Boolean aBoolean) { return Observable.just(aBoolean.hashCode()); //returns observable } }) .toList() //or .toSortedList() .subscribe(new Observer>() { 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

4VCKFDUTm NVTJDQMBZFSFYBNQMF0CTFSWFS public class MusicPlayer { public enum PLAYER_STATE {STOPPED, PLAYING} Observer playerObserver = new Observer() { @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

Slide 16

Slide 16 text

4VCKFDUTm NVTJDQMBZFSFYBNQMF4VCKFDU public class MusicPlayer { PLAYER_STATE currentPlayerState = PLAYER_STATE.STOPPED; BehaviorSubject 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

Slide 17

Slide 17 text

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 getPlayerStateObservable() { return playerStateSubject.asObservable(); }  Use Subscription to control Observer lifecycle! 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

$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

Slide 21

Slide 21 text

$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

Slide 22

Slide 22 text

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.bindUntilEvent(ActivityEvent.PAUSE)) //binds to onPause() .subscribe(onCreateObserver); } } .compose(this.bindUntilEvent(ActivityEvent.DESTROY)) //binds to onDestroy() ... .compose(this.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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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 getUserObservable() { return mUserSubject.asObservable(); } }  There's no automated way to manage lifecycle on the global level! 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD

Slide 25

Slide 25 text

.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 getUserObservable() { return mUserSubject.asObservable(); } } 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD

Slide 26

Slide 26 text

.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 getUserObservable() { return mUserSubject.asObservable(); } } 3FDJQFTJO3Y+BWB GPS"OESPJE](SPLLJOH3Y ]!TBTB@TFLVMJD

Slide 27

Slide 27 text

.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.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

Slide 28

Slide 28 text

.BOBHFSFYBNQMF SFVTJOHPCTFSWBCMFDIBJOT  if you have a part of the chain that repeats itself – extract it in a Transformer () 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

Slide 29

Slide 29 text

.BOBHFSFYBNQMF SFVTJOHPCTFSWBCMFDIBJOT DPOUE Observable.Transformer convertConfigToUser() { return new Observable.Transformer() { @Override public Observable call(Observable 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

Slide 30

Slide 30 text

.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 convertConfigToUser() { return new Observable.Transformer() { @Override public Observable call(Observable 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

Slide 31

Slide 31 text

.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

Slide 32

Slide 32 text

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 or Action1  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

Slide 33

Slide 33 text

%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

Slide 34

Slide 34 text

%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

Slide 35

Slide 35 text

'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