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

Functional on Android

Eric Kok
October 22, 2016

Functional on Android

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.

Eric Kok

October 22, 2016
Tweet

Other Decks in Programming

Transcript

  1. 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
  2. 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
  3. 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
  4. 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
  5. 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
  6. List<String> numbers = Arrays.asList("5", "3", "4", "1", "2");
 Collections.sort(numbers, (s1,

    s2) -> 
 Integer.valueOf(s1).compareTo(Integer.valueOf(s2)));
  7. 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));
 }
  8. 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));
 }
  9. 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));
 }
  10. 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);
 }
 }
  11. 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); 
 

  12. 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); 
 

  13. 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); 
 

  14. 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?
  15. Reactive programming • Sources of data that stream • Combining

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

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

    on emit, completion, error • threading • reactive pull • backpressure
  18. 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);
  19. 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);
  20. 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);
  21. 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);
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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);
  29. Caching in memory: subjects • ReplaySubject • re-emits all •

    BehaviorSubject • re-emits last item • PublishSubject • re-emits none
  30. Lifecycle management • Keep Subject or cache()’s stream in •

    singleton (Repository) • retained fragment • nonConfigurationInstance
  31. 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);
 }
 

  32. 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;
 }
  33. 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());
  34. Observable<Beer> allBeers = rxdb.query(Beer.class); 
 Observable<Beer> piwotekaBeers = rxdb.query(Beer.class, "brewer

    = ?", “Piwoteka"); 
 Observable<Beer> beerWithId8 = rxdb.get(Beer.class, 8);
  35. 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());
  36. 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
  37. 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); }
  38. 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
  39. 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
  40. 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
  41. 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(); }
  42. 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(); }
  43. Here be dragons Observable.create(subscriber -> {
 for (String number :

    numbers) {
 subscriber.onNext(number);
 }
 }); Observable.from(numbers); don’t use .create() use .from()
  44. 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()
  45. 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()
  46. Agera? • Reactive framework from Google Play Movies team •

    + MVP tied Android lifecycle • value-less signals and manual fetching • race conditions • no stream completion/errors
  47. Recommended • RxBinding (view binding) • Retrofit (network calls) •

    RxLifecycle (automatic unsubscriptions) • android-reactive-location (location updates) • RxTuples (pairing model objects) • RxCupboard :)
  48. Jack compiler • Take a real-world app RateBeer • Set

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