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

RxJava Easy Wins - Droidcon NYC 2014

Ron Shapiro
September 21, 2014

RxJava Easy Wins - Droidcon NYC 2014

Everyone keeps talking about Reactive Programming and RxJava, but how is it actually used in Android programming? This talk will discuss the building blocks of reactive programming, Observables, and how to understand the core parts of push-not-pull programming. Learn how to easily do work on a background thread in one line while still updating your UI on the main thread, easily implement a Publish-Subscribe pattern with Observables and Subjects, and take advantage of RxJava to make form-based UIs simple. Finally, we'll look at how using Observables can improve testing and ensure that your code will not block the UI thread if operations are blocked.

Ron Shapiro

September 21, 2014
Tweet

More Decks by Ron Shapiro

Other Decks in Technology

Transcript

  1. Ron Shapiro · @rdshapiro
    DroidconNYC 2014
    Easy wins to help get you in the groove
    RxJava + Android

    View Slide

  2. Quick Survey
    Who has played around with RxJava?
    Who uses RxJava in production?
    compile 'io.reactivex:rxjava:1.+'
    formerly: compile 'com.netflix.rxjava:rxjava-core:+'

    View Slide

  3. What is Rx?
    Let's take a step back.
    Imperative programming:

    View Slide

  4. What is Rx?
    int a = 1;
    int b = a + 2;
    a = 5;
    assertTrue(3, b);
    Let's take a step back.
    Imperative programming:

    View Slide

  5. A "real-world" example of Reactive Programming

    View Slide

  6. A "real-world" example of Reactive Programming
    Excel spreadsheet:

    View Slide

  7. A "real-world" example of Reactive Programming
    Excel spreadsheet:
    A
    1 2
    2 4
    3 =A1 + A2

    View Slide

  8. A "real-world" example of Reactive Programming
    Excel spreadsheet:
    A
    1 2
    2 4
    3 =A1 + A2
    A
    1 2
    2 4
    3 6

    View Slide

  9. A "real-world" example of Reactive Programming
    Excel spreadsheet:
    A
    1 2
    2 4
    3 =A1 + A2
    A
    1 2
    2 4
    3 6
    A
    1 2
    2 5
    3 7

    View Slide

  10. RxJava Contract
    void onNext(T t);
    void onError(Throwable throwable);
    void onCompleted();

    View Slide

  11. RxJava Contract
    void onNext(T t);
    void onError(Throwable throwable);
    void onCompleted();
    T get();
    Iterable get();
    vs. Imperative Programming

    View Slide

  12. RxJava Contract
    void onNext(T t);
    void onError(Throwable throwable);
    void onCompleted();
    T get() throws Throwable;
    Iterable get() throws Throwable;
    vs. Imperative Programming

    View Slide

  13. In Action
    Observable.just(1, 2, 3, 4)
    .subscribe(new Subscriber() {
    @Override
    public void onNext(Integer integer) {
    System.out.println(integer);
    }
    @Override
    public void onCompleted() {
    System.out.println("Done!");
    }
    !
    @Override
    public void onError(Throwable e) {
    System.err.println("An error occurred: " + e.getMessage());
    }
    });
    !
    > 1
    > 2
    > 3
    > 4
    > Done!

    View Slide

  14. Java 8 to
    the Rescue!

    View Slide

  15. Java 8 to
    the Rescue!
    Java 8 to
    the Rescue!

    View Slide

  16. Java 8 to
    the Rescue!
    Java 8 to
    the Rescue!
    Yes, on Android!

    View Slide

  17. Retrolambda
    !
    https://github.com/evant/gradle-retrolambda
    Observable.just(1, 2, 3, 4).subscribe(
    integer -> System.out.println(integer),
    error -> System.err.println("An error occurred: " +
    error.getMessage()),
    () -> System.out.println(“Done!"));

    View Slide

  18. Observable.just(1, 2, 3, 4).subscribe(
    + integer -> System.out.println(integer),
    - System.out::println,
    error -> System.err.println("An error occurred: " +
    error.getMessage()),
    () -> System.out.println("Done!"));
    Retrolambda
    !
    https://github.com/evant/gradle-retrolambda

    View Slide

  19. Observable.just(1, 2, 3, 4).subscribe(
    + integer -> System.out.println(integer),
    - System.out::println,
    error -> System.err.println("An error occurred: " +
    error.getMessage()),
    () -> System.out.println("Done!"));
    Retrolambda
    !
    https://github.com/evant/gradle-retrolambda
    Not only is this more concise, Java 8 lambdas don't have an implicit reference to their enclosing
    block's class like anonymous classes do (unless you explicitly refer to MyClass.this)

    View Slide

  20. So why would I actually use this?
    Ok.

    View Slide

  21. Long running tasks

    View Slide

  22. (Potentially) long running tasks

    View Slide

  23. (Potentially) long running tasks
    db.loadSomeData()
    .subscribe(data -> textView.setText(data.toString));

    View Slide

  24. (Potentially) long running tasks
    db.loadSomeData()
    + .subscribeOn(Schedulers.io())
    + .observeOn(AndroidSchedulers.mainThread())
    .subscribe(data -> textView.setText(data.toString));

    View Slide

  25. (Potentially) long running tasks
    Observable loadSomeData() {
    // loading is un-opinionated about
    // what thread it is running on
    }
    db.loadSomeData()
    + .subscribeOn(Schedulers.io())
    + .observeOn(AndroidSchedulers.mainThread())
    .subscribe(data -> textView.setText(data.toString));

    View Slide

  26. (Potentially) long running tasks
    Observable loadSomeData() {
    // loading is un-opinionated about
    // what thread it is running on
    }
    db.loadSomeData()
    .subscribeOn(Schedulers.newThread())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(data -> textView.setText(data.toString));

    View Slide

  27. Observables + SQL

    View Slide

  28. Observables + SQL
    shameless plug for github.com/venmo/cursor-utils

    View Slide

  29. Observables + SQL
    shameless plug for github.com/venmo/cursor-utils
    IterableCursor: turn a Cursor row into a T
    IterableCursorAdapter adapter = new
    IterableCursorAdapter(context);
    db.loadSomeData()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .subscribe(adapter::changeCursor);

    View Slide

  30. Observables + SQL
    Because IterableCursor extends Iterable,
    you can easily transform your queries into Observables:

    View Slide

  31. Observables + SQL
    db.loadSomeData()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .flatMap(cursor -> Observable.from(cursor))
    .onNext(pojo -> /* ... */);
    Because IterableCursor extends Iterable,
    you can easily transform your queries into Observables:

    View Slide

  32. Observables + SQL
    Because IterableCursor extends Iterable,
    you can easily transform your queries into Observables:
    db.loadSomeData()
    .subscribeOn(Schedulers.io())
    .observeOn(AndroidSchedulers.mainThread())
    .flatMap(cursor -> Observable.from(cursor))
    - .onNext(pojo -> /* ... */);
    + .toList()
    + .onNext(pojoList -> /* ... */);

    View Slide

  33. Observables have a semi-declaritive syntax

    View Slide

  34. Observables have a semi-declaritive syntax
    // detect when the user pauses typing,
    // then search the api
    ViewObservable.text(myTextView)
    .map(TextView::getText)
    .delay(400, TimeUnit.MILLISECONDS)
    .filter(delayedText -> TextUtils.equals(
    delayedText, myTextView.getText()))
    .subscribe(api::search);

    View Slide

  35. Observables have a semi-declaritive syntax
    // detect when the user pauses typing,
    // then search the api
    ViewObservable.text(myTextView)
    .map(TextView::getText)
    + .map(CharSequence::toString())
    .delay(400, TimeUnit.MILLISECONDS)
    .filter(delayedText -> TextUtils.equals(
    delayedText, myTextView.getText()))
    .subscribe(api::search);
    // make sure you're always dealing with immutable objects!

    View Slide

  36. Form Validation

    View Slide

  37. Form Validation
    Observable validName = ViewObservable
    .text(nameTextView)
    .map(view -> TextUtils.isEmpty(view.getText()));
    Observable validTermsOfService =
    ViewObservable.input(tosCheckBox, true);
    Observable validBirthdate = // ...

    View Slide

  38. Form Validation

    View Slide

  39. Form Validation
    Func3 combinator =
    (name, tos, date) -> name && tos && date);
    Observable.combineLatest(validName, validTermsOfService, validBirthdate,
    combinator)
    .subscribe(allValid -> {
    if (allValid) {
    submitButton.setVisibility(View.VISIBLE);
    } else {
    submitButton.setVisibility(View.GONE);
    }
    });

    View Slide

  40. Form Validation
    Func3 combinator =
    (name, tos, date) -> name && tos && date);
    Observable.combineLatest(validName, validTermsOfService, validBirthdate,
    combinator)
    .subscribe(allValid -> {
    if (allValid) {
    submitButton.setVisibility(View.VISIBLE);
    } else {
    submitButton.setVisibility(View.GONE);
    }
    });
    combineLatest and FuncX are type-safe! Adding a new input source in one area but
    not the other will break at compile-time instead of a silent runtime mistake

    View Slide

  41. Composability

    View Slide

  42. sp?
    Composability

    View Slide

  43. API Client

    View Slide

  44. API Client
    Observable getMetaData() {/* ... */}
    Observable postPicture(String url, Bitmap b) {/* ... */}
    Observable linkPostToPermalink(Strind id, Permalink p) {/* ... */}

    View Slide

  45. API Client
    Observable getMetaData() {/* ... */}
    Observable postPicture(String url, Bitmap b) {/* ... */}
    Observable linkPostToPermalink(Strind id, Permalink p) {/* ... */}
    !
    // if any of these fail, i.e. call `onError` instead of `onCompleted`,
    // the subsequent calls are not made
    getMetaData()
    .flatMap(metadata -> postPicture(metadata.getUrl(), bitmap)
    .flatMap(permalink -> linkPostToPermalink(postId, permalink)
    .subscribe(/* ... */);

    View Slide

  46. API Client
    Observable getMetaData() {/* ... */}
    Observable postPicture(String url, Bitmap b) {/* ... */}
    Observable linkPostToPermalink(Strind id, Permalink p) {/* ... */}
    !
    // if any of these fail, i.e. call `onError` instead of `onCompleted`,
    // the subsequent calls are not made
    getMetaData()
    .flatMap(metadata -> postPicture(metadata.getUrl(), bitmap)
    .flatMap(permalink -> linkPostToPermalink(postId, permalink)
    + .subscribeOn(Schedulers.io())
    + .observeOn(AndroidSchedulers.mainThread())
    .subscribe(/* ... */);

    View Slide

  47. API Client

    View Slide

  48. API Client
    Observable first = // ...
    Observable second = // ...
    Observable third = // ...
    Func3 combineFunc = // ...

    View Slide

  49. API Client
    // wait for all 3 to complete
    Observable.combineLatest(first, second, third, combineFunc)
    .subscribe((R r) -> doSomethingAwesome());
    Observable first = // ...
    Observable second = // ...
    Observable third = // ...
    Func3 combineFunc = // ...

    View Slide

  50. API Client
    // wait for all 3 to complete
    Observable.combineLatest(first, second, third, combineFunc)
    .subscribe((R r) -> doSomethingAwesome());
    Observable first = // ...
    Observable second = // ...
    Observable third = // ...
    Func3 combineFunc = // ...
    Make sure each of first, second, and third handles off-loading/concurrency concerns.
    !

    View Slide

  51. RxEventBus

    View Slide

  52. RxEventBus
    Subject // observe a T, emit an R (can be the same)
    AsyncSubject: last emitted item
    BehaviorSubject: most recent + all future items
    PublishSubject: all future items
    ReplaySubject: caches all* items, emits each on subscribing

    View Slide

  53. RxEventBus
    class RxEventBus {
    private Subject subject = PublishSubject.create();
    Subscription subscribe(Action1 super T> action, Class clazz) {
    return subject.ofType(clazz).subscribe(action);
    }
    void post(T t) {
    subject.onNext(t);
    }
    void post(Observable observable) {
    observable.subscribe(subject);
    }
    }

    View Slide

  54. RxEventBus
    class RxEventBus {
    // ...
    /** Forwarding terminal events will terminate your Subject! */
    void post(Observable observable) {
    observable.materialize().map((Notification notification) -> {
    if (notification.isOnNext()) {
    return notification.getValue();
    } else if (notification.isOnError()) {
    return new OnErrorEvent(notification.getThrowable());
    }
    return new OnCompletedEvent();
    }).subscribe(subject);
    }
    }

    View Slide

  55. RxEventBus
    class RxEventBus {
    // ...
    /** Forwarding terminal events will terminate your Subject! */
    void post(Observable observable) {
    observable.materialize().map((Notification notification) -> {
    if (notification.isOnNext()) {
    return notification.getValue();
    } else if (notification.isOnError()) {
    return new OnErrorEvent(notification.getThrowable());
    }
    return new OnCompletedEvent();
    }).subscribe(subject);
    }
    }

    View Slide

  56. Simplified Testing

    View Slide

  57. Simplified Testing
    Observable sleepObservable(long timeout, TimeUnit unit) {
    return Observable.create((Subscriber super T> subscriber) -> {
    SystemClock.sleep(unit.toMillis(timeout));
    subscriber.onCompleted();
    });
    }

    View Slide

  58. Simplified Testing
    Observable sleepObservable(long timeout, TimeUnit unit) {
    return Observable.create((Subscriber super T> subscriber) -> {
    SystemClock.sleep(unit.toMillis(timeout));
    subscriber.onCompleted();
    });
    }
    Database db = Mockito.spy(db);
    doAnswer(mockedMethod -> {
    return sleepObservable(2, SECONDS)
    .concatWith(mockedMethod.callRealMethod());
    }).when(db).loadSomeData()

    View Slide

  59. Simplified Testing
    AtomicBoolean wasClicked = new AtomicBoolean(false);
    new Thread(() -> {
    SystemClock.sleep(1000);
    assertTrue(wasClicked.get());
    }).start();
    onView(withId(R.id.my_view)
    .perform(setOnClickListener((v) -> wasClicked.set(true))
    .perform(click();
    // if your data is being loaded on the main-thread,
    // the view will not be clicked

    View Slide

  60. Look familiar?
    UNEXPECTED TOP-LEVEL EXCEPTION:
    java.lang.IllegalArgumentException: method ID not in [0, 0xffff]: 65536
    at com.android.dx.merge.DexMerger$6.updateIndex(DexMerger.java:501)
    at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:276)
    at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:490)
    at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:167)
    at com.android.dx.merge.DexMerger.merge(DexMerger.java:188)
    at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:439)
    at com.android.dx.command.dexer.Main.runMonoDex(Main.java:287)
    at com.android.dx.command.dexer.Main.run(Main.java:230)
    at com.android.dx.command.dexer.Main.main(Main.java:199)
    at com.android.dx.command.Main.main(Main.java:103)

    View Slide

  61. Look familiar?
    UNEXPECTED TOP-LEVEL EXCEPTION:
    java.lang.IllegalArgumentException: method ID not in [0, 0xffff]: 65536
    at com.android.dx.merge.DexMerger$6.updateIndex(DexMerger.java:501)
    at com.android.dx.merge.DexMerger$IdMerger.mergeSorted(DexMerger.java:276)
    at com.android.dx.merge.DexMerger.mergeMethodIds(DexMerger.java:490)
    at com.android.dx.merge.DexMerger.mergeDexes(DexMerger.java:167)
    at com.android.dx.merge.DexMerger.merge(DexMerger.java:188)
    at com.android.dx.command.dexer.Main.mergeLibraryDexBuffers(Main.java:439)
    at com.android.dx.command.dexer.Main.runMonoDex(Main.java:287)
    at com.android.dx.command.dexer.Main.run(Main.java:230)
    at com.android.dx.command.dexer.Main.main(Main.java:199)
    at com.android.dx.command.Main.main(Main.java:103)
    RxJava is pretty big - you may/may not need ProGuard

    View Slide

  62. Ron Shapiro
    [email protected]
    @rdshapiro
    !
    venmo.com/ronshapiro
    Questions?

    View Slide