Share
Embed
Get Reactive - Devoxx
The RxJava hype is still in full swing and almost everything gets Rxified. At least since the stable RxAndroid 1.0 release it’s time to take RxAndroid seriously!
In this session you will learn what RxJava is about, how to use it on Android and how RxAndroid can be used to build maintainable Android Apps from networking with Retrofit2 to UI with the new RxBinding and RxLifecycle libraries powered by MVP.
If you haven’t had the time to Get Reactive, we’ll provide a quick introduction why you should use RxAndroid in your current and next projects.
Pascal Welsch
November 12, 2015
Transcript
56 slides
-
@rittmeyerw, @passsy #Devoxx #GetReactive RxJava – a library for composing
asynchronous and event-based programs using observable sequences for the Java VM. https://github.com/ReactiveX/RxJava -
@rittmeyerw, @passsy #Devoxx #GetReactive Simple Observable Observable<String> countObservable = Observable.from(Arrays.asList(new
Integer[]{2, 3, 5, 7})) .filter(n -> n % 2 != 0) .count() .map(n -> String.format("%d item(s)", n)); -
@rittmeyerw, @passsy #Devoxx #GetReactive Simple Subscriber countObservable.subscribe(new Subscriber<String>() { @Override
public void onCompleted() { Log.d("get_reactive", "Done!"); } @Override public void onError(Throwable e) { Log.e("get_reactive", "Yikes!", e); } @Override public void onNext(String s) { Log.d("get_reactive", s); } }); -
@rittmeyerw, @passsy #Devoxx #GetReactive Simple Subscriber countObservable.subscribe(new Subscriber<String>() { @Override
public void onCompleted() { Log.d("get_reactive", "Done!"); } @Override public void onError(Throwable e) { Log.e("get_reactive", "Yikes!", e); } @Override public void onNext(String s) { Log.d("get_reactive", s); } }); -
@rittmeyerw, @passsy #Devoxx #GetReactive Creating Observables • Basic • Observable.from()
• Observable.just() • Observable.range() • Timing based • Observable.interval() • Observable.timer() • Custom • Observable.create() -
@rittmeyerw, @passsy #Devoxx #GetReactive Creating Observables Observable.create(new Observable.OnSubscribe<Integer>() { public
void call(Subscriber<? super Integer> s) { try { Random r = new Random(); for (int i = 0; i < 10; i++) { s.onNext(r.nextInt()); } s.onCompleted(); } catch (Throwable t) { s.onError(t); } } }); -
@rittmeyerw, @passsy #Devoxx #GetReactive Creating Observables Observable.create(new Observable.OnSubscribe<Integer>() { public
void call(Subscriber<? super Integer> s) { try { Random r = new Random(); for (int i = 0; i < 10; i++) { s.onNext(r.nextInt()); } s.onCompleted(); } catch (Throwable t) { s.onError(t); } } }); -
@rittmeyerw, @passsy #Devoxx #GetReactive Creating Observables Observable.create(new Observable.OnSubscribe<Integer>() { public
void call(Subscriber<? super Integer> s) { try { Random r = new Random(); for (int i = 0; i < 10; i++) { if (!s.isUnsubscribed()) { s.onNext(r.nextInt()); } else { break; } } // ... -
@rittmeyerw, @passsy #Devoxx #GetReactive Consuming Observables • subscribe() overloaded •
Be careful to not leak anything • anonymous inner classes • forgetting to unsubscribe -
@rittmeyerw, @passsy #Devoxx #GetReactive Processing Chain • Transformers • map,
flatMap, scan • Filters • filter, throttle, take, skip • Conditions • amb, skipWhile, takeWhile • Combination • concat, zip, combineLatest • Aggregate • count, reduce, toList, toMap -
@rittmeyerw, @passsy #Devoxx #GetReactive Backpressure • Observable to fast for
consumer • Can happen anywhere in the chain • Some possible actions: • Throttle • Buffer • request(n) upstream items -
@rittmeyerw, @passsy #Devoxx #GetReactive Off the Main thread • Schedulers
• subscribeOn() • Schedulers.io() • Schedulers.computation() • Schedulers.newThread() • Schedulers.fromExecutor() • observeOn • AndroidSchedulers.mainThread() -
@rittmeyerw, @passsy #Devoxx #GetReactive Testing Observables • Create special cases
• Observable.error() • Observable.empty() • Observable.never() • Use TestSubscriber -
@rittmeyerw, @passsy #Devoxx #GetReactive Debugging Observables • Side effect operators
• doOnNext() • doOnSubscribe() • … • Hooks • RxJavaObservableExecutionHook • RxJavaSchedulersHook • RxJavaErrorHandler -
@rittmeyerw, @passsy #Devoxx #GetReactive What is RxAndroid? // RxAndroid contains
the AndroidScheduler (main thread) // few releases compile 'io.reactivex:rxandroid:1.0.1' // additionally add specific (latest) version of RxJava compile 'io.reactivex:rxjava:1.0.14' -
@rittmeyerw, @passsy #Devoxx #GetReactive Make Listeners Reactive // typical Android
widget listener final Button myButton = (Button) findViewById(R.id.myButton); myButton.setOnClickListener(new View.OnClickListener() { @Override public void onClick(final View v) { triggerAction(); } }); -
@rittmeyerw, @passsy #Devoxx #GetReactive Make Listeners Reactive final Observable<Void> myButtonClickObservable
= Observable.create(subscriber -> { myButton.setOnClickListener(v -> { if (!subscriber.isUnsubscribed()) { subscriber.onNext(v); } }); }); myButtonClickObservable .subscribe(v -> triggerAction()); unsubscribing not handled! lamda for Observable.OnSubscribe<T> -
@rittmeyerw, @passsy #Devoxx #GetReactive Make Listeners Reactive final Observable<Void> myButtonClickObservable
= Observable.create(subscriber -> { myButton.setOnClickListener(v -> { if (!subscriber.isUnsubscribed()) { subscriber.onNext(null); } }); }); myButtonClickObservable .delay(100, TimeUnit.MILLISECONDS) .observeOn(Schedulers.io()) .subscribe(v -> triggerAction()); -
@rittmeyerw, @passsy #Devoxx #GetReactive RxBinding compile 'com.jakewharton.rxbinding:rxbinding:0.3.0' RxView.clicks(myButton) .subscribe(v ->
triggerAction()); Others and more RxView, RxTextView, RxAdapter, RxAdapterView, RxAutoCompleteTextView, RxCheckedTextView, RxCompoundButton, RxMenuItem, RxSeekBar, RxViewGroup, RxProgressBar, RxRadioGroup, RxRatingBar, RxRecyclerView, RxRecyclerViewAdapter, RxSearchView, RxToolbar, ... -
@rittmeyerw, @passsy #Devoxx #GetReactive compile 'com.jakewharton.rxbinding:rxbinding:0.3.0' RxView.clicks(myButton) .subscribe(v -> triggerAction());
Others and more RxView, RxTextView, RxAdapter, RxAdapterView, RxAutoCompleteTextView, RxCheckedTextView, RxCompoundButton, RxMenuItem, RxSeekBar, RxViewGroup, RxProgressBar, RxRadioGroup, RxRatingBar, RxRecyclerView, RxRecyclerViewAdapter, RxSearchView, RxToolbar, ... RxBinding :rxbinding-support-v4 :rxbinding-appcompat-v7 :rxbinding-design :rxbinding-recyclerview-v7 :rxbinding-leanback-v17 -
@rittmeyerw, @passsy #Devoxx #GetReactive The login view TextView username_tv =
(TextView) findViewById(R.id.username); TextView password_tv = (TextView) findViewById(R.id.password); Observable<CharSequence> rxUsername = RxTextView.textChanges(username_tv); Observable<CharSequence> rxPassword = RxTextView.textChanges(password_tv); too easy... -
@rittmeyerw, @passsy #Devoxx #GetReactive Build a registration view! TextView username_tv
= (TextView) findViewById(R.id.username); TextView password_tv = (TextView) findViewById(R.id.password); // optional field TextView fullName_tv = (TextView) findViewById(R.id.fullName); Observable<CharSequence> rxUsername = RxTextView.textChanges(username_tv); Observable<CharSequence> rxPassword = RxTextView.textChanges(password_tv); Observable<CharSequence> rxFullName = RxTextView.textChanges(fullName_tv); Observable.combineLatest( rxUsername, rxPassword, rxFullName, RegistrationModel::new) .filter(RegistrationModel::isValid) .subscribe(LoginActivity::enableSubmitButton); never called when fullName not entered -
@rittmeyerw, @passsy #Devoxx #GetReactive Build a registration view! TextView username_tv
= (TextView) findViewById(R.id.username); TextView password_tv = (TextView) findViewById(R.id.password); // optional field TextView fullName_tv = (TextView) findViewById(R.id.fullName); Observable<CharSequence> rxUsername = RxTextView.textChanges(username_tv); Observable<CharSequence> rxPassword = RxTextView.textChanges(password_tv); Observable<CharSequence> rxFullName = RxTextView.textChanges(fullName_tv) .mergeWith(Observable.just("")); Observable.combineLatest( rxUsername, rxPassword, rxFullName, RegistrationModel::new) // triggers now without entering a fullName .filter(RegistrationModel::isValid) .subscribe(LoginActivity::enableSubmitButton); emits "" after subscribe -
@rittmeyerw, @passsy #Devoxx #GetReactive GitHub repository search Using Retrofit 2
for the network calls -
@rittmeyerw, @passsy #Devoxx #GetReactive the Android Way searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override public boolean onQueryTextChange(final String query) { if (!TextUtils.isEmpty(query)) { mGitApiService.searchRepositories(query).enqueue(callback); } return true; } @Override public boolean onQueryTextSubmit(final String query) { return false; } }); -
@rittmeyerw, @passsy #Devoxx #GetReactive the Android Way Callback<GitHubResponse> callback =
new Callback<GitHubResponse>() { @Override public void onFailure(final Throwable t) { showEmptyErrorView(t.getMessage()); } @Override public void onResponse( Response<GitHubResponse> response, Retrofit retrofit) { if (response.isSuccess()) { showRepositories(response.body().getItems()); } else { try { showEmptyErrorView(response.errorBody().string()); } catch (IOException e) { showEmptyErrorView(response.message()); // [\)\]\}]* -
@rittmeyerw, @passsy #Devoxx #GetReactive the RxBeginner solution RxSearchView.queryTextChanges(searchView) .filter(charSequence ->
!TextUtils.isEmpty(charSequence)) .flatMap(charSequence -> { return mGitApiService .searchRepositoriesObservable(charSequence.toString()) .subscribeOn(Schedulers.io()); }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(response -> { showRepositories(response.getItems()); }, throwable -> { showEmptyErrorView(throwable.getMessage()); }); -
@rittmeyerw, @passsy #Devoxx #GetReactive The RxBeginner solution RxSearchView.queryTextChanges(searchView) .filter(charSequence ->
!TextUtils.isEmpty(charSequence)) .flatMap(charSequence -> { return searchRepositories(charSequence); }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(response -> { showRepositories(response.getItems()); }, throwable -> { showEmptyErrorView(throwable.getMessage()); }); -
@rittmeyerw, @passsy #Devoxx #GetReactive keep order with concatMap RxSearchView.queryTextChanges(searchView) .filter(charSequence
-> !TextUtils.isEmpty(charSequence)) .concatMap(charSequence -> { return searchRepositories(charSequence); }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(response -> { showRepositories(response.getItems()); }, throwable -> { showEmptyErrorView(throwable.getMessage()); }); -
@rittmeyerw, @passsy #Devoxx #GetReactive without backpressure (1/3) 11-12 04:24:48.897 V:
searching: 11-12 04:24:48.916 V: empty view 11-12 04:24:49.581 V: searching: d 11-12 04:24:49.598 V: requesting d 11-12 04:24:49.624 D: --> GET /search/repositories?q=d HTTP/1.1 11-12 04:24:49.741 V: searching: de 11-12 04:24:49.743 V: requesting de 11-12 04:24:49.910 V: searching: dev 11-12 04:24:50.026 V: searching: devo 11-12 04:24:50.155 V: searching: devox 11-12 04:24:50.307 V: searching: devoxx 11-12 04:24:50.667 D: <-- HTTP/1.1 200 OK (1042ms, -1-byte body) 11-12 04:24:50.778 V: got data 11-12 04:24:50.779 D: --> GET /search/repositories?q=de HTTP/1.1 11-12 04:24:50.793 V: requesting dev -
@rittmeyerw, @passsy #Devoxx #GetReactive without backpressure (2/3) 11-12 04:24:50.307 V:
searching: devoxx 11-12 04:24:50.667 D: <-- HTTP/1.1 200 OK (1042ms, -1-byte body) 11-12 04:24:50.778 V: got data 11-12 04:24:50.779 D: --> GET /search/repositories?q=de HTTP/1.1 11-12 04:24:50.793 V: requesting dev 11-12 04:24:51.218 D: <-- HTTP/1.1 200 OK (438ms, -1-byte body) 11-12 04:24:51.223 V: got data 11-12 04:24:51.223 D: --> GET /search/repositories?q=dev HTTP/1.1 11-12 04:24:51.243 V: requesting devo 11-12 04:24:51.728 D: <-- HTTP/1.1 200 OK (504ms, -1-byte body) 11-12 04:24:51.734 V: got data 11-12 04:24:51.741 V: requesting devox 11-12 04:24:51.741 D: --> GET /search/repositories?q=devo HTTP/1.1 11-12 04:24:52.067 D: <-- HTTP/1.1 200 OK (325ms, -1-byte body) 11-12 04:24:52.077 V: got data -
@rittmeyerw, @passsy #Devoxx #GetReactive without backpressure (3/3) 11-12 04:24:51.741 V:
requesting devox 11-12 04:24:51.741 D: --> GET /search/repositories?q=devo HTTP/1.1 11-12 04:24:52.067 D: <-- HTTP/1.1 200 OK (325ms, -1-byte body) 11-12 04:24:52.077 V: got data 11-12 04:24:52.078 D: --> GET /search/repositories?q=devox HTTP/ 1.1 11-12 04:24:52.096 V: requesting devoxx 11-12 04:24:52.360 D: <-- HTTP/1.1 200 OK (282ms, -1-byte body) 11-12 04:24:52.369 V: got data 11-12 04:24:52.369 D: --> GET /search/repositories?q=devoxx HTTP/ 1.1 11-12 04:24:52.663 D: <-- HTTP/1.1 200 OK (293ms, -1-byte body) 11-12 04:24:52.670 V: got data 2.3s until the response is visible -
@rittmeyerw, @passsy #Devoxx #GetReactive handle backpressure RxSearchView.queryTextChanges(searchView) .filter(charSequence -> !TextUtils.isEmpty(charSequence))
.throttleLast(100, TimeUnit.MILLISECONDS) .debounce(200, TimeUnit.MILLISECONDS) .onBackpressureLatest() .concatMap(charSequence -> { return searchRepositories(charSequence); }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(response -> { showRepositories(response.getItems()); }, throwable -> { showEmptyErrorView(throwable.getMessage()); }); prevents request for every character only one query waiting while request running -
@rittmeyerw, @passsy #Devoxx #GetReactive with throttleLast and debounce 11-12 04:21:35.661
V: searching: 11-12 04:21:35.875 V: empty view 11-12 04:21:37.098 V: searching: d 11-12 04:21:37.261 V: searching: de 11-12 04:21:37.430 V: searching: dev 11-12 04:21:37.572 V: searching: devo 11-12 04:21:37.740 V: searching: devox 11-12 04:21:37.903 V: searching: devoxx 11-12 04:21:38.177 V: requesting devoxx 11-12 04:21:38.186 D: --> GET /search/repositories?q=devoxx HTTP/ 1.1 11-12 04:21:39.024 D: <-- HTTP/1.1 200 OK (837ms, -1-byte body) 11-12 04:21:39.131 V: got data 1.3s until the response is visible -
@rittmeyerw, @passsy #Devoxx #GetReactive request while typing 11-12 04:32:37.580 V:
searching: 11-12 04:32:37.875 V: empty view 11-12 04:32:38.355 V: searching: d 11-12 04:32:38.521 V: searching: de 11-12 04:32:38.573 V: requesting d 11-12 04:32:38.582 D: --> GET /search/repositories?q=d HTTP/1.1 11-12 04:32:38.706 V: searching: dev 11-12 04:32:38.882 V: searching: devo 11-12 04:32:39.147 V: searching: devox 11-12 04:32:39.172 V: requesting devo 11-12 04:32:39.313 V: searching: devoxx 11-12 04:32:39.379 D: <-- HTTP/1.1 200 OK (797ms, -1-byte body) 11-12 04:32:39.457 V: searching: devoxx 11-12 04:32:39.692 V: searching: devoxx t 11-12 04:32:39.784 V: searching: devoxx ta -
@rittmeyerw, @passsy #Devoxx #GetReactive with throttleLast and debounce 11-12 04:32:39.964
V: searching: devoxx tal 11-12 04:32:39.972 V: got data 11-12 04:32:39.975 D: --> GET /search/repositories?q=devo HTTP/1.1 11-12 04:32:40.000 V: requesting devoxx 11-12 04:32:40.089 V: searching: devoxx talk 11-12 04:32:40.185 V: searching: devoxx talks 11-12 04:32:40.264 D: <-- HTTP/1.1 200 OK (289ms, -1-byte body) 11-12 04:32:40.270 V: got data 11-12 04:32:40.271 D: --> GET /search/repositories?q=devoxx%20 HTTP/1.1 11-12 04:32:40.346 V: searching: devoxx talks 11-12 04:32:40.506 V: searching: devoxx talks a 11-12 04:32:40.574 D: <-- HTTP/1.1 200 OK (303ms, -1-byte body) 11-12 04:32:40.582 V: got data 11-12 04:32:40.612 V: searching: devoxx talks ab -
@rittmeyerw, @passsy #Devoxx #GetReactive with throttleLast and debounce 11-12 04:32:40.574
D: <-- HTTP/1.1 200 OK (303ms, -1-byte body) 11-12 04:32:40.582 V: got data 11-12 04:32:40.612 V: searching: devoxx talks ab 11-12 04:32:40.705 V: searching: devoxx talks abo 11-12 04:32:40.831 V: searching: devoxx talks abou 11-12 04:32:40.977 V: searching: devoxx talks about 11-12 04:32:41.278 V: requesting devoxx talks about 11-12 04:32:41.279 D: --> GET /search/repositories?q=devoxx %20talks%20about HTTP/1.1 11-12 04:32:41.451 D: <-- HTTP/1.1 200 OK (172ms, -1-byte body) 11-12 04:32:41.452 V: got data -
@rittmeyerw, @passsy #Devoxx #GetReactive handle error RxSearchView.queryTextChanges(searchView) .filter(charSequence -> !TextUtils.isEmpty(charSequence))
.throttleLast(100, TimeUnit.MILLISECONDS) .debounce(200, TimeUnit.MILLISECONDS) .onBackpressureLatest() .concatMap(charSequence -> { return searchRepositories(charSequence); }) .observeOn(AndroidSchedulers.mainThread()) .subscribe(response -> { showRepositories(response.getItems()); }, throwable -> { showEmptyErrorView(throwable.getMessage()); }); 1. error not handled 2. handled here 3. completes Observable with error -> no more emits! -
@rittmeyerw, @passsy #Devoxx #GetReactive handle error private Observable<GitHubResponse> searchRepositories(CharSequence charSequence)
{ return mGitApiService .searchRepositoriesObservable(charSequence.toString()) .subscribeOn(Schedulers.io()) .onErrorResumeNext(throwable -> { // handle error somehow, change UI return Observable.empty(); }); } Observable completes without error and without result rx.Single doesn’t support this error handling :( -
@rittmeyerw, @passsy #Devoxx #GetReactive Mock network requests // retrofit 2
interface @GET("/search/repositories") Observable<GitHubResponse<Repository>> searchRepositoriesObservable(@Query("q") String name); // implementation @Override public Observable<GitHubResponse<Repository>> searchRepositoriesObservable(final String name) { GitHubResponse<Repository> response = new GitHubResponse<>(testRepo1, testRepo2, testRepoN); return Observable.just(response) .delay(1000, TimeUnit.MILLISECONDS); } -
@rittmeyerw, @passsy #Devoxx #GetReactive cancel requests Call unsubscribe in #onStop();
mSearchViewSubscription.unsubscribe(); or use github.com/trello/RxLifecycle for it Use a Presenter (MVP) to continue the request during a configuration change Recommendation: Mosby or Nucleus github.com/sockeqwe/mosby github.com/konmik/nucleus -
@rittmeyerw, @passsy #Devoxx #GetReactive +Wolfram Rittmeyer @rittmeyerw +Pascal Welsch @passsy
Don’t forget to subscribe to our social media streams (no error handling required)