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

FUNCTIONAL ON ANDROID: LAMBDAS, RX AND STREAMS IN YOUR APP - ERIC KOK

1fa9cb8c7997c8c4d3d251fb5e41f749?s=47 Realm
October 22, 2016

FUNCTIONAL ON ANDROID: LAMBDAS, RX AND STREAMS IN YOUR APP - ERIC KOK

On Android, more and more developers are attracted to the functional programming-style concept of declarative data manipulation using lambdas. Java 8 has a new steams API, but it's limited to Android N. Backports exist, but it's RxJava that's all the rage, with its elegant threading solution. How do we use lambdas, streams and Rx effectively on Android? Orientation changes and background tasks? I propose to stop worrying about the lifecycle and cache your way into a blissful user experience.

1fa9cb8c7997c8c4d3d251fb5e41f749?s=128

Realm

October 22, 2016
Tweet

Transcript

  1. Functional on Android Lambdas, Rx and streams in your app

    Eric Kok
  2. None
  3. Functional in Java

  4. Functional in Java Lambdas? Java 8? Rx? Streams?

  5. List<String> numbers = Arrays.asList("5", "3", "4", "1", "2");
 Collections.sort(numbers, new

    Comparator<String>() {
 @Override
 public int compare(String s1, String s2) {
 Integer i1 = Integer.valueOf(s1);
 Integer i2 = Integer.valueOf(s2);
 return i1.compareTo(i2);
 }
 }); Lambdas
  6. List<String> numbers = Arrays.asList("5", "3", "4", "1", "2");
 Collections.sort(numbers, new

    Comparator<String>() {
 @Override
 public int compare(String s1, String s2) {
 Integer i1 = Integer.valueOf(s1);
 Integer i2 = Integer.valueOf(s2);
 return i1.compareTo(i2);
 }
 }); Lambdas
  7. List<String> numbers = Arrays.asList("5", "3", "4", "1", "2");
 Collections.sort(numbers,
 @Override


    public int compare(String s1, String s2) {
 Integer i1 = Integer.valueOf(s1);
 Integer i2 = Integer.valueOf(s2);
 return i1.compareTo(i2);
 }
 ); Lambdas
  8. List<String> numbers = Arrays.asList("5", "3", "4", "1", "2");
 Collections.sort(numbers,
 


    ( s1, s2) {
 Integer i1 = Integer.valueOf(s1);
 Integer i2 = Integer.valueOf(s2);
 return i1.compareTo(i2);
 }
 ); Lambdas
  9. List<String> numbers = Arrays.asList("5", "3", "4", "1", "2");
 Collections.sort(numbers,
 


    ( s1, s2) -> {
 Integer i1 = Integer.valueOf(s1);
 Integer i2 = Integer.valueOf(s2);
 return i1.compareTo(i2);
 }
 ); Lambdas
  10. List<String> numbers = Arrays.asList("5", "3", "4", "1", "2");
 Collections.sort(numbers, (s1,

    s2) -> 
 Integer.valueOf(s1).compareTo(Integer.valueOf(s2)));
  11. List<String> numbers = Arrays.asList("5", "3", "4", "1", "2");
 Collections.sort(numbers, (s1,

    s2) -> 
 Integer.valueOf(s1).compareTo(Integer.valueOf(s2))); private int compareAsInt(String s1, String s2) {
 return Integer.valueOf(s1).compareTo(Integer.valueOf(s2));
 }
  12. List<String> numbers = Arrays.asList("5", "3", "4", "1", "2");
 Collections.sort(numbers, (s1,

    s2) -> compareAsInt(s1, s2)); private int compareAsInt(String s1, String s2) {
 return Integer.valueOf(s1).compareTo(Integer.valueOf(s2));
 }
  13. List<String> numbers = Arrays.asList("5", "3", "4", "1", "2");
 Collections.sort(numbers, this::compareAsInt);

    private int compareAsInt(String s1, String s2) {
 return Integer.valueOf(s1).compareTo(Integer.valueOf(s2));
 }
  14. Streaming List<String> numbers = Arrays.asList("5", "3", "4", "1", "2");
 


    for (String number : numbers) {
 Integer i = Integer.valueOf(number); // convert to int
 if (i % 2 == 0) { // filter even
 log(i);
 }
 }
  15. Streaming List<String> numbers = Arrays.asList("5", "3", "4", "1", "2");
 


    numbers.stream() // start streaming
 .map(Integer::valueOf) // convert to int
 .filter(i -> i % 2 == 0) // filter even
 .forEach(this::log); 
 

  16. Streaming Java 8 and Android N (minSdk 24) List<String> numbers

    = Arrays.asList("5", "3", "4", "1", "2");
 
 numbers.stream() // start streaming
 .map(Integer::valueOf) // convert to int
 .filter(i -> i % 2 == 0) // filter even
 .forEach(this::log); 
 

  17. Streaming StreamSupport List<String> numbers = Arrays.asList("5", "3", "4", "1", "2");


    
 StreamSupport.stream(numbers) // start streaming
 .map(Integer::valueOf)
 .filter(i -> i % 2 == 0)
 .forEach(this::log); 
 

  18. Streaming List<String> numbers = Arrays.asList("5", "3", "4", "1", "2");
 


    Observable.from(numbers) // create observable
 .map(Integer::valueOf)
 .filter(i -> i % 2 == 0)
 .subscribe(this::log); // start emitting 
 
 RxJava?
  19. Reactive programming • Sources of data that stream • Combining

    and transforming streams • Think: spreadsheet
  20. • Collection streaming (push) • Java 8 / Android N

    / Streamsupport • Reactive interfaces (pull) • RxJava 1 and 2 • Akka streams • Reactor Core • Java 9 Flow
  21. RxJava • Streaming data with • transformations • lifecycle callbacks

    on emit, completion, error • threading • reactive pull • backpressure
  22. List<Integer> ids = Arrays.asList(5, 3, 4, 1, 2);
 
 Observable.from(ids)


    
 .map(i -> Network.blockingCall(i))
 .filter(item -> item.shouldShow())
 
 .subscribe(this::log);
  23. List<Integer> ids = Arrays.asList(5, 3, 4, 1, 2);
 
 Observable.from(ids)


    
 .map(i -> Network.getBeer(i))
 .filter(beer -> beer.rating > 0.8F)
 
 .subscribe(this::showBeer);
  24. List<Integer> ids = Arrays.asList(5, 3, 4, 1, 2);
 
 Observable.from(ids)


    .subscribeOn(io()) // emit on io
 .map(i -> Network.getBeer(i))
 .filter(beer -> beer.rating > 0.8F) 
 .observeOn(mainThread()) // switch to main thread
 .subscribe(this::showBeer);
  25. List<Integer> ids = Arrays.asList(5, 3, 4, 1, 2);
 
 Observable.from(ids)


    .subscribeOn(io())
 .map(i -> Network.getBeer(i))
 .filter(beer -> beer.rating > 0.8F) .take(1) // only request 1
 .observeOn(mainThread())
 .subscribe(this::showBeer);
  26. Rx on Android

  27. RxView.clicks(button1)
 
 
 .scan(0, (integer, click) -> integer + 1)

    // running count
 .map(i -> Integer.toString(i)) // int to string
 
 .subscribe(RxTextView.text(textview1)); RxBinding and RxAndroid
  28. RxView.clicks(button1)
 
 .subscribeOn(Schedulers.computation()) // switch to computation
 .scan(0, (integer, click)

    -> integer + 1) // running count
 .map(i -> Integer.toString(i)) // int to string
 
 .subscribe(RxTextView.text(textview1)); RxBinding and RxAndroid
  29. RxView.clicks(button1)
 
 .subscribeOn(Schedulers.computation()) // switch to computation
 .scan(0, (integer, click)

    -> integer + 1) // running count
 .map(i -> Integer.toString(i)) // int to string
 
 .subscribe(RxTextView.text(textview1)); RxBinding and RxAndroid CalledFromWrongThreadException
  30. RxBinding and RxAndroid RxView.clicks(button1)
 
 .subscribeOn(Schedulers.computation()) // switch to computation


    .scan(0, (integer, click) -> integer + 1) // running count
 .map(i -> Integer.toString(i)) // int to string
 .observeOn(AndroidSchedulers.mainThread()) // switch to main thread
 .subscribe(RxTextView.text(textview1)); CalledFromWrongThreadException
  31. RxBinding and RxAndroid RxView.clicks(button1)
 
 .subscribeOn(Schedulers.computation()) // switch to computation


    .scan(0, (integer, click) -> integer + 1) // running count
 .map(i -> Integer.toString(i)) // int to string
 .observeOn(AndroidSchedulers.mainThread()) // switch to main thread
 .subscribe(RxTextView.text(textview1)); IllegalStateException: Must be called from the main thread
  32. RxBinding and RxAndroid RxView.clicks(button1)
 .subscribeOn(AndroidSchedulers.mainThread()) // emit on computation
 .observeOn(Schedulers.computation())

    // switch to computation
 .scan(0, (integer, click) -> integer + 1) // running count
 .map(i -> Integer.toString(i)) // int to string
 .observeOn(AndroidSchedulers.mainThread()) // switch to main thread
 .subscribe(RxTextView.text(textview1)); IllegalStateException: Must be called from the main thread
  33. Observable.combineLatest(
 RxCompoundButton.checkedChanges(agreeTermsCheck),
 RxCompoundButton.checkedChanges(noCommentCheck),
 RxTextView.textChanges(reviewEdit).map(s -> s.length() > 80), 
 (agreeTerms,

    noComment, hasReview) -> agreeTerms && (noComment || hasReview))
 .subscribe(RxView.enabled(submitButton));
  34. RxTextView.textChanges(searchEdit) 
 .debounce(1, TimeUnit.SECONDS, mainThread()) // delay emission 
 .subscribe(this::startQuery));

  35. RxTextView.textChanges(searchEdit)
 
 .debounce(1, TimeUnit.SECONDS, io()) // on background
 .flatMap(query ->

    Network.searchBeer(query.toString()))
 
 .subscribe(this::showBeers);
  36. RxTextView.textChanges(searchEdit)
 .subscribeOn(mainThread()) // on ui
 .debounce(1, TimeUnit.SECONDS, io())
 .flatMap(query ->

    Network.searchBeer(query.toString()))
 .observeOn(mainThread()) // to ui
 .subscribe(this::showBeers);
  37. BehaviorSubject<Float> syncProgress = BehaviorSubject.create();
 syncProgress.onNext(0.15F); // post event
 syncProgress .subscribe(RxView.progress(progressBar));


  38. BehaviorSubject<Float> syncProgress = BehaviorSubject.create();
 syncProgress.onNext(0.15F);
 syncProgress .subscribe(RxView.progress(progressBar)) 
 syncProgress
 .map(f

    -> f >= 1F)
 .subscribe(RxView.visibility(syncedItemList));
  39. Caching

  40. Caching in memory: subjects • ReplaySubject • re-emits all •

    BehaviorSubject • re-emits last item • PublishSubject • re-emits none
  41. Caching in memory: cache() Observable<Beer> cached = Network.getBeer(8)
 .cache();
 cached.subscribe(this::showBeerHere)

    cached.subscribe(this::showBeerThere)
  42. Caching in memory: cache() Observable<Beer> cached = Network.getBeer(8)
 .replay(30, TimeUnit.MINUTES).autoConnect();


    cached.subscribe(this::showBeerHere) cached.subscribe(this::showBeerThere
  43. Lifecycle management • Keep Subject or cache()’s stream in •

    singleton (Repository) • retained fragment • nonConfigurationInstance
  44. protected void onCreate(Bundle savedInstanceState) {
 ...
 
 Network.findByBrewer("Piwoteka")
 .subscribeOn(io())
 .observeOn(mainThread())


    .subscribe(this::showBeers); }
 

  45. private Observable<Beer> beerCall; protected void onCreate(Bundle savedInstanceState) {
 ...
 


    if (beerCall == null) {
 beerCall = Network.findByBrewer("Piwoteka").cache();
 }
 beerCall
 .subscribeOn(io())
 .observeOn(mainThread())
 .subscribe(this::showBeers);
 }
 

  46. private Observable<Beer> beerCall; protected void onCreate(Bundle savedInstanceState) {
 ...
 


    beerCall = (Observable<Beer>) getLastNonConfigurationInstance();
 if (beerCall == null) {
 beerCall = Network.findByBrewer("Piwoteka").cache();
 }
 beerCall
 .subscribeOn(io())
 .observeOn(mainThread())
 .subscribe(this::showBeers);
 }
 
 @Override
 public Object onRetainNonConfigurationInstance() {
 return beerCall;
 }
  47. RxCupboard • Rx wrapper around Hugo’s Cupboard • Minimal boilerplate

    • Relies heavy on defaults public class Beer {
 public Long _id;
 public String brewer;
 public String name;
 } cupboard().register(Beer.class); // one-time registration RxDatabase rxdb = RxCupboard.withDefault( new SQLiteOpenHelper(context).getWritableDatabase());
  48. Observable<Beer> allBeers = rxdb.query(Beer.class); 
 Observable<Beer> piwotekaBeers = rxdb.query(Beer.class, "brewer

    = ?", “Piwoteka"); 
 Observable<Beer> beerWithId8 = rxdb.get(Beer.class, 8);
  49. source.subscribe(beer -> rxdb.put()); 
 source.doOnNext(rxdb.put()).map(...).subscribe(...); rxdb.putRx(beer).map(...).subscribe(...);

  50. source.subscribe(beer -> rxdb.put()); 
 source.doOnNext(rxdb.put()).map(...).subscribe(...); rxdb.putRx(beer).map(...).subscribe(...); Network.findByBrewer("Piwoteka")
 .map(rxdb::putRx) // insert/update

    every beer
 .toList()
 .subscribe(this::showBeerList);
  51. Observable<DatabaseChange> changes = rxdb.changes(); 
 Observable<DatabaseChange<Beer>> changes = rxdb.changes(Beer.class);

  52. Observable<DatabaseChange> changes = rxdb.changes(); 
 Observable<DatabaseChange<Beer>> changes = rxdb.changes(Beer.class); Observable<Beer>

    changedBeers = rxdb.changes(Beer.class)
 .map(change -> change.entity());
  53. Observable<DatabaseChange> changes = rxdb.changes(); 
 Observable<DatabaseChange<Beer>> changes = rxdb.changes(Beer.class); Observable<Beer>

    addedBeers = rxdb.changes(Beer.class)
 .ofType(DatabaseChange.DatabaseInsert.class)
 .map(insertion -> (Beer) insertion.entity()); // cast :( Observable<Beer> changedBeers = rxdb.changes(Beer.class)
 .map(change -> change.entity());
  54. Caching in db protected void onCreate(Bundle savedInstanceState) {
 ...
 Observable<Beer>

    db = rxdb.get(Beer.class, 8) .filter(beer -> beer != null); Observable<Beer> fresh = Network.getBeer(8) .doOnNext(beer -> rxdb.put()); } Only cached or fresh entity
  55. Caching in db Only cached or fresh entity protected void

    onCreate(Bundle savedInstanceState) {
 ...
 Observable<Beer> db = rxdb.get(Beer.class, 8) .filter(beer -> beer != null); Observable<Beer> fresh = Network.getBeer(8) .doOnNext(beer -> rxdb.put()); Observable.concat(db, fresh) .subscribeOn(io())
 .observeOn(mainThread())
 .subscribe(this::showBeer); }
  56. Caching in db protected void onCreate(Bundle savedInstanceState) {
 ...
 Observable<Beer>

    db = rxdb.get(Beer.class, 8) .filter(beer -> beer != null); Observable<Beer> fresh = Network.getBeer(8) .doOnNext(beer -> rxdb.put()); Observable.concat(db, fresh).first() .subscribeOn(io())
 .observeOn(mainThread())
 .subscribe(this::showBeer); } Only cached or fresh entity
  57. Caching in db protected void onCreate(Bundle savedInstanceState) {
 ... Observable<Beer>

    db = rxdb.query(Beer.class, "brewer = ?", “Piwoteka”); Observable<Beer> fresh = Network.findByBrewer("Piwoteka") .map(rxdb::putRx); Observable.merge(db, fresh) .subscribeOn(io())
 .observeOn(mainThread())
 .subscribe(this::showBeers); } Combine cached and fresh entities
  58. Caching in db protected void onCreate(Bundle savedInstanceState) {
 ... Observable<Beer>

    db = rxdb.query(Beer.class, "brewer = ?", “Piwoteka”); Observable<Beer> fresh = Network.findByBrewer("Piwoteka") .map(rxdb::putRx); Observable.merge(db, fresh).distinct() .subscribeOn(io())
 .observeOn(mainThread())
 .subscribe(this::showBeers); } Combine cached and fresh entities
  59. Here be dragons • Beware of leaking subscriptions • Hot

    observables (no onComplete) • Long running operations (temporary leak) private Subscription sub; protected void onStart() { ... sub = hotSource.subscribe(this::showBeers) } protected void onStop() { sub.unsubscribe(); }
  60. Here be dragons • Beware of leaking subscriptions • Hot

    observables (no onComplete) • Long running operations (temporary leak) private Subscription sub; protected void onResume() { ... sub = hotSource.subscribe(this::showBeers) } protected void onPause() { sub.unsubscribe(); }
  61. Here be dragons Observable.create(subscriber -> {
 for (String number :

    numbers) {
 subscriber.onNext(number);
 }
 }); Observable.from(numbers); don’t use .create() use .from()
  62. Here be dragons Observable.create(subscriber -> {
 someButton.setOnClickListener(view -> {
 subscriber.onNext(null);


    });
 }); Observable.fromEmitter(subscriber -> {
 subscriber.onNext(null);
 }, Emitter.BackpressureMode.DROP); don’t use .create() use .fromEmitter()
  63. Here be dragons Observable.just(Network.blockingCall(“query")); Observable.fromCallable(() -> Network.blockingCall("query")); beware of blocking

    code use .fromCallable()
  64. RxJava 2 • the future! • build on Java 8

    reactive-streams interfaces • explicit backpressure • Flowable vs Observable (and Subject vs Processor) • .subscribe() doesn't return Subscriber • creation via .create()
  65. Agera? • Reactive framework from Google Play Movies team •

    + MVP tied Android lifecycle • value-less signals and manual fetching • race conditions • no stream completion/errors
  66. Jack compiler compileSdkVersion 24 
 jackOptions {
 enabled true
 }

  67. Jack compiler • Take a real-world app RateBeer • Set

    compileSdk and remove retrolambda compileSdkVersion 24 
 jackOptions {
 enabled true
 } 0 errors