Share
Embed
Get Reactive
Given at Droidcon UK 2015
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
October 29, 2015
Transcript
49 slides
-
RxJava – a library for composing asynchronous and event-based programs
using observable sequences for the Java VM. https://github.com/ReactiveX/RxJava -
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)); -
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); } }); -
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); } }); -
Creating Observables • Basic ◦ Observable.from() ◦ Observable.just() ◦ Observable.range()
• Timing based ◦ Observable.interval() ◦ Observable.timer() • Custom ◦ Observable.create() -
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); } } }); -
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); } } }); -
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; } } // ... -
Consuming Observables • subscribe() overloaded • Be careful to not
leak anything ◦ anonymous inner classes ◦ forgetting to unsubscribe -
Processing Chain • Transformers ◦ map, flatMap, scan • Filters
◦ filter, throttle, take, skip • Conditions ◦ amb, skipWhile, takeWhile • Combination ◦ concat, zip, combineLatest • Aggregate ◦ count, reduce, toList, toMap -
Off the Main thread • Schedulers ◦ subscribeOn() ▪ Schedulers.io()
▪ Schedulers.computation() ▪ Schedulers.newThread() ▪ Schedulers.fromExecutor() ◦ observeOn ▪ AndroidSchedulers.mainThread() -
Backpressure • Observable to fast for consumer • Can happen
anywhere in the chain • Some possible actions: ◦ Throttle ◦ Buffer ◦ request(n) upstream items -
Testing Observables • Create special cases ◦ Observable.error() ◦ Observable.empty()
◦ Observable.never() • Use TestSubscriber -
Debugging Observables • Side effect operators ◦ doOnNext() ◦ doOnSubscribe()
◦ … • Hooks ◦ RxJavaObservableExecutionHook ◦ RxJavaSchedulersHook ◦ RxJavaErrorHandler -
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' -
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(); } }); -
Make Listeners Reactive final Observable<Void> myButtonClickObservable = Observable.create(subscriber -> {
myButton.setOnClickListener(v -> { if (!subscriber.isUnsubscribed()) { subscriber.onNext(v); } }); }); myButtonClickObservable .subscribe(v -> triggerAction()); lamda for Observable.OnSubscribe<T> unsubscribing not handled! -
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()); -
compile 'com.jakewharton.rxbinding:rxbinding:0.3.0' RxView.clicks(myButton) .subscribe(v -> triggerAction()); RxBinding Others and more
RxView, RxTextView, RxAdapter, RxAdapterView, RxAutoCompleteTextView, RxCheckedTextView, RxCompoundButton, RxMenuItem, RxSeekBar, RxViewGroup, RxProgressBar, RxRadioGroup, RxRatingBar, RxRecyclerView, RxRecyclerViewAdapter, RxSearchView, RxToolbar, ... -
compile 'com.jakewharton.rxbinding:rxbinding:0.3.0' RxView.clicks(myButton) .subscribe(v -> triggerAction()); RxBinding Others and more
RxView, RxTextView, RxAdapter, RxAdapterView, RxAutoCompleteTextView, RxCheckedTextView, RxCompoundButton, RxMenuItem, RxSeekBar, RxViewGroup, RxProgressBar, RxRadioGroup, RxRatingBar, RxRecyclerView, RxRecyclerViewAdapter, RxSearchView, RxToolbar, ... rxbinding-support-v4 rxbinding-appcompat-v7 rxbinding-design rxbinding-recyclerview-v7 rxbinding-leanback-v17 -
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... -
Build a registration view! TextView username_tv = (TextView) findViewById(R.id.username); TextView
password_tv = (TextView) findViewById(R.id.password); TextView fullName_tv = (TextView) findViewById(R.id.fullName); // optional field 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 -
Build a registration view! TextView username_tv = (TextView) findViewById(R.id.username); TextView
password_tv = (TextView) findViewById(R.id.password); TextView fullName_tv = (TextView) findViewById(R.id.fullName); // optional field 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 -
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; } }); -
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()); // [\)\]\}]* -
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()); }); -
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()); }); -
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()); }); -
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 -
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! -
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 :( -
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); } -
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