Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Get Reactive

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
Tweet

More Decks by Pascal Welsch

Other Decks in Programming

Transcript

  1. RxJava – a library for composing asynchronous and event-based programs

    using observable sequences for the Java VM. https://github.com/ReactiveX/RxJava
  2. 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));
  3. 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); } });
  4. 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); } });
  5. Creating Observables • Basic ◦ Observable.from() ◦ Observable.just() ◦ Observable.range()

    • Timing based ◦ Observable.interval() ◦ Observable.timer() • Custom ◦ Observable.create()
  6. 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); } } });
  7. 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); } } });
  8. 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; } } // ...
  9. Consuming Observables • subscribe() overloaded • Be careful to not

    leak anything ◦ anonymous inner classes ◦ forgetting to unsubscribe
  10. Processing Chain • Transformers ◦ map, flatMap, scan • Filters

    ◦ filter, throttle, take, skip • Conditions ◦ amb, skipWhile, takeWhile • Combination ◦ concat, zip, combineLatest • Aggregate ◦ count, reduce, toList, toMap
  11. zip

  12. Off the Main thread • Schedulers ◦ subscribeOn() ▪ Schedulers.io()

    ▪ Schedulers.computation() ▪ Schedulers.newThread() ▪ Schedulers.fromExecutor() ◦ observeOn ▪ AndroidSchedulers.mainThread()
  13. Backpressure • Observable to fast for consumer • Can happen

    anywhere in the chain • Some possible actions: ◦ Throttle ◦ Buffer ◦ request(n) upstream items
  14. Debugging Observables • Side effect operators ◦ doOnNext() ◦ doOnSubscribe()

    ◦ … • Hooks ◦ RxJavaObservableExecutionHook ◦ RxJavaSchedulersHook ◦ RxJavaErrorHandler
  15. 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'
  16. 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(); } });
  17. 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!
  18. 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());
  19. 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, ...
  20. 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
  21. 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...
  22. 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
  23. 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
  24. 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; } });
  25. 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()); // [\)\]\}]*
  26. 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()); });
  27. 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()); });
  28. 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()); });
  29. 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
  30. 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!
  31. 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 :(
  32. 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); }
  33. 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