$30 off During Our Annual Pro Sale. View Details »

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

    View Slide

  2. +Wolfram Rittmeyer
    @rittmeyerw
    +Pascal Welsch
    @passsy

    View Slide

  3. Reactive Extensions

    View Slide

  4. RxJava – a library for composing
    asynchronous and event-based programs
    using observable sequences for the Java VM.
    https://github.com/ReactiveX/RxJava

    View Slide

  5. Reactive Extensions

    View Slide

  6. Simple Observable
    Observable 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));

    View Slide

  7. Simple Subscriber
    countObservable.subscribe(new Subscriber() {
    @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);
    }
    });

    View Slide

  8. Simple Subscriber
    countObservable.subscribe(new Subscriber() {
    @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);
    }
    });

    View Slide

  9. RxJava in Detail

    View Slide

  10. Creating Observables
    ● Basic
    ○ Observable.from()
    ○ Observable.just()
    ○ Observable.range()
    ● Timing based
    ○ Observable.interval()
    ○ Observable.timer()
    ● Custom
    ○ Observable.create()

    View Slide

  11. Creating Observables
    Observable.create(new Observable.OnSubscribe() {
    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);
    }
    }
    });

    View Slide

  12. Creating Observables
    Observable.create(new Observable.OnSubscribe() {
    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);
    }
    }
    });

    View Slide

  13. Creating Observables
    Observable.create(new Observable.OnSubscribe() {
    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;
    }
    }
    // ...

    View Slide

  14. Consuming Observables
    ● subscribe() overloaded
    ● Be careful to not leak anything
    ○ anonymous inner classes
    ○ forgetting to unsubscribe

    View Slide

  15. Processing Chain
    ● Transformers
    ○ map, flatMap, scan
    ● Filters
    ○ filter, throttle, take, skip
    ● Conditions
    ○ amb, skipWhile, takeWhile
    ● Combination
    ○ concat, zip, combineLatest
    ● Aggregate
    ○ count, reduce, toList, toMap

    View Slide

  16. flatMap

    View Slide

  17. combineLatest

    View Slide

  18. zip

    View Slide

  19. Off the Main thread
    ● Schedulers
    ○ subscribeOn()
    ■ Schedulers.io()
    ■ Schedulers.computation()
    ■ Schedulers.newThread()
    ■ Schedulers.fromExecutor()
    ○ observeOn
    ■ AndroidSchedulers.mainThread()

    View Slide

  20. Error Handling
    ● Observable.onErrorResumeNext()
    ● Observable.onErrorReturn()
    ● yourObserver.onError()

    View Slide

  21. Backpressure
    ● Observable to fast for consumer
    ● Can happen anywhere in the chain
    ● Some possible actions:
    ○ Throttle
    ○ Buffer
    ○ request(n) upstream items

    View Slide

  22. Testing Observables
    ● Create special cases
    ○ Observable.error()
    ○ Observable.empty()
    ○ Observable.never()
    ● Use TestSubscriber

    View Slide

  23. Debugging Observables
    ● Side effect operators
    ○ doOnNext()
    ○ doOnSubscribe()
    ○ …
    ● Hooks
    ○ RxJavaObservableExecutionHook
    ○ RxJavaSchedulersHook
    ○ RxJavaErrorHandler

    View Slide

  24. RxAndroid

    View Slide

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

    View Slide

  26. Reactive Android UI

    View Slide

  27. 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();
    }
    });

    View Slide

  28. Make Listeners Reactive
    final Observable myButtonClickObservable =
    Observable.create(subscriber -> {
    myButton.setOnClickListener(v -> {
    if (!subscriber.isUnsubscribed()) {
    subscriber.onNext(v);
    }
    });
    });
    myButtonClickObservable
    .subscribe(v -> triggerAction());
    lamda for
    Observable.OnSubscribe
    unsubscribing not handled!

    View Slide

  29. Make Listeners Reactive
    final Observable myButtonClickObservable =
    Observable.create(subscriber -> {
    myButton.setOnClickListener(v -> {
    if (!subscriber.isUnsubscribed()) {
    subscriber.onNext(null);
    }
    });
    });
    myButtonClickObservable
    .delay(100, TimeUnit.MILLISECONDS)
    .observeOn(Schedulers.io())
    .subscribe(v -> triggerAction());

    View Slide

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

    View Slide

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

    View Slide

  32. Combine UI events

    View Slide

  33. The login view
    TextView username_tv = (TextView) findViewById(R.id.username);
    TextView password_tv = (TextView) findViewById(R.id.password);
    Observable rxUsername = RxTextView.textChanges(username_tv);
    Observable rxPassword = RxTextView.textChanges(password_tv);
    too easy...

    View Slide

  34. 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 rxUsername = RxTextView.textChanges(username_tv);
    Observable rxPassword = RxTextView.textChanges(password_tv);
    Observable rxFullName = RxTextView.textChanges(fullName_tv);
    Observable.combineLatest(
    rxUsername, rxPassword, rxFullName, RegistrationModel::new)
    .filter(RegistrationModel::isValid)
    .subscribe(LoginActivity::enableSubmitButton);
    never called when fullName not entered

    View Slide

  35. combineLatest

    View Slide

  36. 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 rxUsername = RxTextView.textChanges(username_tv);
    Observable rxPassword = RxTextView.textChanges(password_tv);
    Observable 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

    View Slide

  37. network requests
    triggered by UI

    View Slide

  38. GitHub repository
    search
    Using Retrofit 2 for the
    network calls

    View Slide

  39. 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;
    }
    });

    View Slide

  40. the Android Way
    Callback callback
    = new Callback() {
    @Override public void onFailure(final Throwable t) {
    showEmptyErrorView(t.getMessage());
    }
    @Override
    public void onResponse(
    Response response, Retrofit retrofit) {
    if (response.isSuccess()) {
    showRepositories(response.body().getItems());
    } else {
    try {
    showEmptyErrorView(response.errorBody().string());
    } catch (IOException e) {
    showEmptyErrorView(response.message());
    // [\)\]\}]*

    View Slide

  41. 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());
    });

    View Slide

  42. 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());
    });

    View Slide

  43. 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());
    });

    View Slide

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

    View Slide

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

    View Slide

  46. handle error
    private Observable
    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 :(

    View Slide

  47. Mock network requests
    // retrofit 2 interface
    @GET("/search/repositories")
    Observable>
    searchRepositoriesObservable(@Query("q") String name);
    // implementation
    @Override
    public Observable>
    searchRepositoriesObservable(final String name) {
    GitHubResponse response =
    new GitHubResponse<>(testRepo1, testRepo2, testRepoN);
    return Observable.just(response)
    .delay(1000, TimeUnit.MILLISECONDS);
    }

    View Slide

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

    View Slide

  49. +Wolfram Rittmeyer
    @rittmeyerw
    +Pascal Welsch
    @passsy

    View Slide