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

Fantastic Async and Where to Find them

Parth
January 17, 2019

Fantastic Async and Where to Find them

As Android developers in the modern age, we're spoiled for choice when it comes to asynchronous tools: RxJava, LiveData, Kotlin Coroutines and now with AndroidX, ListenableFuture! But how do these architectures scale? How easy is it to onboard new developers? How performant is it?

In this whirlwind expedition, we'll adventure in search of fantastic async solutions to your concurrency problems, spending time to discuss how to implement a flexible, scalable architecture for your application, big or small.

Parth

January 17, 2019
Tweet

More Decks by Parth

Other Decks in Technology

Transcript

  1. 2

  2. 3

  3. Table of Contents I. Background II. Surveying the Landscape III.

    Fantastic Async IV. Questions (& Answers?) 4
  4. Q: Why do devs use async tools? 5 (why don't

    we write everything blockingly?)
  5. Q: Why do devs use async tools? A: Because network

    calls on the UI thread crash my app ☹
  6. Q: Why do devs use async tools? A: Because network

    calls on the UI thread crash my app ☹
  7. Q: Why do devs use async tools? A #2: The

    world is async, and we have to live in it A: Because network calls on the UI thread crash my app ☹
  8. Scoring System - Flexibility 1. post messages to the UI

    thread 2. execute commands from a BG thread 3. choose where to observe messages 14
  9. Scoring System - Composition 1. blocking completion 2. chain multiple

    operations • bonus: fan-out + fan-in 3. bridge into framework from Callable • bonus: bridge from generic callback 15
  10. Scoring System - Granularity 1. send [1, ∞] events to

    observer 2. cancel the async task(s) 3. robust error handling a. per-event b. per-stream 16
  11. LiveData -- Flexibility Scorecard post to UI thread execute on

    background thread choose execution context 18
  12. LiveData -- Composition -- Chaining 20 fun flatMapEquivalent(ld: LiveData<Int>): LiveData<String>

    { return Transformations.switchMap(ld) { input: Int !-> val ld2 = MutableLiveData<String>() ld2.value = input.toString() return@switchMap ld2 } }
  13. LiveData -- Granularity Scorecard send [1, ∞] events to observer

    cancel the async task(s) robust error handling • per-event • per-stream 21
  14. LiveData - Use Cases 23 Use if... • (Google) frameworks

    are only sources of async • No complex async interactions Shines as... • event bus to UI thread • public API for ViewModel or Repository
  15. ListenableFuture -- Flexibility Scorecard post to UI thread execute on

    background thread choose execution context 25
  16. ListenableFuture -- Flexibility -- UI Thread 26 fun observeCompletionOnUiThread_vanilla() {

    val future: ListenableFuture<WorkInfo> = WorkManager.getInstance().getWorkInfoById(UUID.randomUUID()) future.addListener(Runnable { try { val workInfo: WorkInfo = future.get() "//here be dragons"!! } catch (e: Exception) { when (e) { is CancellationException "-> Log.e(tag, "canceled!") is ExecutionException "-> Log.e(tag, "future"::get threw the error") is InterruptedException "-> Log.e(tag, "blocking interrupted :(") else "-> throw e } } }, mainThreadExecutor) }
  17. ListenableFuture -- Flexibility -- UI Thread 27 fun observeCompletionOnUiThread_guava() {

    val future: ListenableFuture<WorkInfo> = WorkManager.getInstance().getWorkInfoById(UUID.randomUUID()) Futures.addCallback(future, object : FutureCallback<WorkInfo> { override fun onSuccess(result: WorkInfo?) { Log.d(tag, "success!") } override fun onFailure(t: Throwable) {""...} }, MoreExecutors.newSequentialExecutor(mainThreadExecutor)) }
  18. ListenableFuture -- Composition - Fan Out+In 29 fun flatmap_fanout_fanin_guava(): ListenableFuture<List<State.SUCCESS">>

    { val wm = WorkManager.getInstance() return FluentFuture.from(wm.getWorkInfosByTag("tag")) .transformAsync(AsyncFunction { works: List<WorkInfo>? !-> if (works "== null) Futures.immediateFailedFuture(RuntimeException()) else { val cancels = mutableListOf<ListenableFuture<State.SUCCESS">>() for (work in works) { cancels += wm.cancelWorkById(work.id).result } Futures.allAsList(cancels) } }, backgroundExecutor) }
  19. ListenableFuture -- Composition - Bridging 30 fun bridgeFromCallback(): ListenableFuture<Int> {

    val lfExec = MoreExecutors.listeningDecorator(backgroundExecutor) val listenableFuture: ListenableFuture<Int> = lfExec.submit(Callable { val latch = CountDownLatch(1) var value = 0 someAsyncCallbackFunction { input: Int !-> latch.countDown() value = input } latch.await() return@Callable value }) return listenableFuture }
  20. ListenableFuture -- Granularity Scorecard send [1, ∞] events to observer

    cancel the async task(s) robust error handling • per-event • per-stream 31
  21. ListenableFuture - Use Cases 33 Use if... • already using

    Guava • don't need multiple 
 events/emissions Shines as... • bridge from "legacy" Future • public API for libraries or internal components
  22. Coroutines -- Flexibility Scorecard post to UI thread execute on

    background thread choose execution context 35
  23. Coroutines - Flexibility 36 fun observeUiThread(textView: TextView) { MainScope().launch(Dispatchers.Main) {

    val user: User = withContext(Dispatchers.IO) { retrofit.randomNetworkCall() } textView.text = "ID: $user.id" } }
  24. Coroutines - Composition 39 suspend fun first(): Int { delay(50L)

    return 1 } suspend fun second(): Int { delay(55L) return 2 } suspend fun third(): Int { delay(65L) return 3 }
  25. Coroutines - Composition 40 fun chainingSeq(scope: CoroutineScope): Job { return

    scope.launch { val result = first() + second() + third() "// ^^^^^^ takes 50+55+65=170ms to execute } } suspend fun first(): Int {""...} suspend fun second(): Int {""...} suspend fun third(): Int {""...}
  26. Coroutines - Composition 41 fun chainingParallel(scope: CoroutineScope): Job { return

    scope.launch { val one: Deferred<Int> = async { first() } val two = async { second() } val three = async { third() } val result = one.await() + two.await() + three.await() "// ^^^^^^ takes 65ms to execute } } fun chainingSeq(scope: CoroutineScope): Job suspend fun first(): Int {""...} suspend fun second(): Int {""...} suspend fun third(): Int {""...}
  27. Coroutines - Composition 42 suspend fun <T> retrofitCall(request: () "->

    Call<T>): Response<T> { return suspendCoroutine { continuation !-> request().enqueue(object : Callback<T> { override fun onFailure(call: Call<T>, t: Throwable) { continuation.resumeWithException(t) } override fun onResponse(call: Call<T>, response: Response<T>) { if (response.isSuccessful) { continuation.resume(response) } else { continuation.resumeWithException(HttpException(response)) } } }) } } https://geoffreymetais.github.io/code/coroutines/
  28. Coroutines - Composition 43 suspend fun <T> retrofitCall(request: () "->

    Call<T>): Response<T> { return suspendCoroutine { continuation !-> request().enqueue(object : Callback<T> { override fun onFailure(call: Call<T>, t: Throwable) { continuation.resumeWithException(t) } override fun onResponse(call: Call<T>, response: Response<T>) { if (response.isSuccessful) { continuation.resume(response) } else { continuation.resumeWithException(HttpException(response)) } } }) } } https://geoffreymetais.github.io/code/coroutines/
  29. Coroutines - Composition 44 suspend fun <T> retrofitCall(request: () "->

    Call<T>): Response<T> { return suspendCoroutine { continuation !-> request().enqueue(object : Callback<T> { override fun onFailure(call: Call<T>, t: Throwable) { continuation.resumeWithException(t) } override fun onResponse(call: Call<T>, response: Response<T>) { if (response.isSuccessful) { continuation.resume(response) } else { continuation.resumeWithException(HttpException(response)) } } }) } } https://geoffreymetais.github.io/code/coroutines/
  30. Coroutines - Composition 45 suspend fun <T> retrofitCall(request: () "->

    Call<T>): Response<T> { return suspendCoroutine { continuation !-> request().enqueue(object : Callback<T> { override fun onFailure(call: Call<T>, t: Throwable) { continuation.resumeWithException(t) } override fun onResponse(call: Call<T>, response: Response<T>) { if (response.isSuccessful) { continuation.resume(response) } else { continuation.resumeWithException(HttpException(response)) } } }) } } https://geoffreymetais.github.io/code/coroutines/
  31. Coroutines -- Granularity Scorecard send [1, ∞] events to observer

    cancel the async task(s) robust error handling • per-event • per-stream 46
  32. Coroutines - Composition 47 fun makeApiCall() { MainScope().launch(Dispatchers.Main) { val

    stringResponse = retrofitCall { apiClient.someNetworkCall() } } } suspend fun <T> retrofitCall(request: () "-> Call<T>): Response<T> {""...}
  33. Coroutines - Granularity 48 fun makeApiCallTryCatch() { MainScope().launch(Dispatchers.Main) { val

    stringResponse: Response<String> = try { retrofitCall { apiClient.someNetworkCall() } } catch (e: HttpException) { "//handle exception Response.success(200, "{}") } } }
  34. Coroutines - Use Cases 50 Use if... • 100% Kotlin

    • no need for mutex/SPSC Shines as... • "under the hood" framework • handle async in tests
  35. RxJava -- Flexibility Scorecard post to UI thread execute on

    background thread choose execution context 52
  36. RxJava - Flexibility 53 val retrofit = Retrofit() fun rxFlexibility()

    { retrofit.randomNetworkCall() "//executes on BG thread .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) 
 "//^^^^^^^^ flip to UI thread .subscribe({ result !-> "/*do something"*/ }, 
 "//^^^^^^^^ runs on UI { err: Throwable !-> "/*do something"*/ }) }
  37. RxJava - Composition 55 fun rxComposition(file: File): List<Any> { return

    Observable.create { emitter: ObservableEmitter<String> !-> BufferedReader(FileReader(file)).use { reader !-> emitter.onNext(reader.readLine()) } } .map { UUID.randomUUID() } .flatMap({ uuid !-> retrofit.randomNetworkCall2(uuid) }, 3) "//fans out 3 active "rails" until queue is drained .toList() "//fan-in; returns Single<List<Foo">> .blockingGet() }
  38. RxJava -- Granularity Scorecard send [1, ∞] events to observer

    cancel the async task(s) robust error handling • per-event • per-stream 56
  39. RxJava - Granularity 57 fun rxGranularity() { lateinit var disposable:

    Disposable disposable = Observable.fromIterable('a'"..'z') .flatMap { c !-> Observable.just( c.toString(), "//"a" c.toString().repeat(2), "//"aa" c.toString().repeat(3) "//"aaa" ) } .takeUntil { str !-> str.toUpperCase().contains("QQ") } "//cancels .onErrorResumeNext { err: Throwable !-> Observable.just("???") } .subscribe("/*something"*/) disposable.dispose() "//cancels, if not already completed }
  40. RxJava - Use Cases 59 Use if... • complex async

    composition • learning curves mean nothing to you Shines as... • Model-View-{Flavor} • wire async through different layers
  41. So what do I use in my app? A: It's

    complicated. ¯\_(ツ)_/¯
  42. Links/Sources/Inspiration/Future Reading 63 • GH: musichin/reactivelivedata bit.ly/2Rudxn6 • Guava -

    ListenableFuture bit.ly/2QOtob3 • KotlinConf '17:
 Intro to Coroutines youtu.be/_hfBv0a09Jc • KotlinLang.org: 
 Guide to Coroutines bit.ly/2HfwLs3 • Geoffrey Métais - Coroutines bit.ly/2AMIp84 • Parth @ Droidcon Berlin '18:
 RxJava Extended: Custom Operators youtu.be/TqrevX2Gtxc