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

Better Async With Kotlin Coroutines

Better Async With Kotlin Coroutines

Android application development is inherently asynchronous. Even the simplest Android application requires the developer to track asynchronous callbacks of the activity, often leading to the infamous callback hell. In this talk, we will have a look at the latest addition to the solutions for dealing with asynchronous programming on Android; Kotlin Coroutines. We'll learn about what coroutines are, how they differ from other models and how to use it with Kotlin on Android.

Erik Hellman

April 20, 2018
Tweet

More Decks by Erik Hellman

Other Decks in Programming

Transcript

  1. BETTER ASYNC ON ANDROID WITH
    KOTLIN COROUTINES
    Erik Hellman - @ErikHellman
    https://speakerdeck.com/erikhellman/better-async-with-kotlin-coroutines

    View Slide

  2. –Developers
    “Async is hard.”

    View Slide

  3. We all suck at multi tasking!

    View Slide

  4. // This must run on a background thread
    @WorkerThread
    fun loadTweets(query: String): List {
    // Load tweets matching the search query
    }
    // This must run on the main thread
    @MainThread
    fun showTweets(tweets: List) {
    // Display tweets
    }

    View Slide

  5. inner class LoadTweetAsyncTask : AsyncTask>() {
    override fun doInBackground(vararg params: String): List {
    return loadTweets(params[0])
    }
    override fun onPostExecute(result: List?) {
    showTweets(result)
    }
    }

    View Slide

  6. val disposable = Single_
    .fromCallable { loadTweets("#AndroidMakers") }_
    .subscribeOn(Schedulers.io())_
    .observeOn(AndroidSchedulers.mainThread())_
    .subscribe { tweets ->_
    showTweets(tweets)_
    }_
    _
    disposable.dispose()_

    View Slide

  7. val disposable = Single_
    .fromCallable { loadTweets("#AndroidMakers") }_
    .subscribeOn(Schedulers.io())_
    .observeOn(AndroidSchedulers.mainThread())_
    .subscribe(_
    {_tweets -> showTweets(tweets)_}_
    {_error -> showError(error)_})_
    _
    disposable.dispose()_

    View Slide

  8. val disposable = Single_
    .fromCallable { loadTweets("#AndroidMakers") }_
    .subscribeOn(Schedulers.io())_
    .observeOn(AndroidSchedulers.mainThread())_
    .subscribe(_
    {_tweets -> showTweets(tweets)_}_
    {_error -> showError(error)_})_
    _
    disposable.dispose()_

    View Slide

  9. val disposable = Single_
    .fromCallable { loadTweets("#AndroidMakers") }_
    .subscribeOn(Schedulers.io())_
    .observeOn(AndroidSchedulers.mainThread())_
    .subscribe(_
    {_tweets -> showTweets(tweets)_}_
    {_error -> showError(error)_})_
    _
    disposable.dispose()_

    View Slide

  10. val disposable = Single_
    .fromCallable { loadTweets("#AndroidMakers") }_
    .subscribeOn(Schedulers.io())_
    .observeOn(AndroidSchedulers.mainThread())_
    .subscribe(_
    {_tweets -> showTweets(tweets)_}_
    {_error -> showError(error)_})_
    _
    disposable.dispose()_

    View Slide

  11. val disposable = Single_
    .fromCallable { loadTweets("#AndroidMakers") }_
    .subscribeOn(Schedulers.io())_
    .observeOn(AndroidSchedulers.mainThread())_
    .subscribe(_
    {_tweets -> showTweets(tweets)_}_
    {_error -> showError(error)_})_
    _
    disposable.dispose()_

    View Slide

  12. WHAT WE WANT
    val tweets = loadTweets(”#AndroidMakers”)
    showTweets(tweets)

    View Slide

  13. WHAT WE CAN HAVE
    load {
    loadTweets(“#AndroidMakers”)
    } then {
    showTweets(it)
    }
    STAY TUNED!

    View Slide

  14. Coroutines = State Machine + Queue

    View Slide

  15. WAT?

    View Slide

  16. LET’S TRY A DIFFERENT ANALOGY!

    View Slide

  17. View Slide

  18. class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    // Restore state from savedInstanceState
    }
    override fun onSaveInstanceState(outState: Bundle?) {
    super.onSaveInstanceState(outState)
    // Save the state of your app
    }
    }

    View Slide

  19. Activity Bundle
    The “process” of the user The state of the process

    View Slide

  20. Activity
    Bundle
    Coroutine
    Continuation

    View Slide

  21. CONTINUATION?
    interface Continuation {
    val context: CoroutineContext
    fun resume(value: T)
    fun resumeWithException(exception: Throwable)
    }

    View Slide

  22. BUT WHAT IS A COROUTINE, REALLY?

    View Slide

  23. –Donald Knuth
    “Subroutines are special cases of ... coroutines.”

    View Slide

  24. Pause d'eau…

    View Slide

  25. launch {.
    val tweets = loadTweets("#AndroidMakers")
    }.

    View Slide

  26. launch(context = CommonPool) {.
    val tweets = loadTweets("#AndroidMakers")
    }.
    CoroutineContext - How to run this coroutine, including which thread

    View Slide

  27. override fun onCreate(savedInstanceState: Bundle?) {.
    super.onCreate(savedInstanceState).
    setContentView(R.layout.activity_main).
    launch(context = CommonPool) {.
    val tweets = loadTweets("#AndroidMakers").
    }.
    }.

    View Slide

  28. val job = launch(context = CommonPool) {.
    val tweets = loadTweets("#AndroidMakers").
    }.
    job.cancel().

    View Slide

  29. val job = launch(context = CommonPool) {.
    val tweets = loadTweets("#AndroidMakers").
    launch(context = UI) {.
    showTweets(tweets).
    }.
    }.
    .
    job.cancel(). Cancelled with parent!
    Switch context (and thread)

    View Slide

  30. public actual fun launch(.
    context: CoroutineContext = DefaultDispatcher,.
    start: CoroutineStart = CoroutineStart.DEFAULT,.
    parent: Job? = null,.
    block: suspend CoroutineScope.() -> Unit.
    ): Job.

    View Slide

  31. public actual fun launch(
    context: CoroutineContext = DefaultDispatcher,.
    start: CoroutineStart = CoroutineStart.DEFAULT,
    parent: Job? = null,
    block: suspend CoroutineScope.() -> Unit
    ): Job
    WHAT THREAD TO RUN ON

    View Slide

  32. public actual fun launch(
    context: CoroutineContext = DefaultDispatcher,
    start: CoroutineStart = CoroutineStart.DEFAULT,.
    parent: Job? = null,
    block: suspend CoroutineScope.() -> Unit
    ): Job.
    START NOW OR LAZILY?

    View Slide

  33. public actual fun launch(
    context: CoroutineContext = DefaultDispatcher,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    parent: Job? = null,
    block: suspend CoroutineScope.() -> Unit
    ): Job
    SPECIFY A PARENT COROUTINE

    View Slide

  34. public actual fun launch(
    context: CoroutineContext = DefaultDispatcher,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    parent: Job? = null,
    block: suspend CoroutineScope.() -> Unit.
    ): Job
    THE FUNCTION TO RUN

    View Slide

  35. val deferred = async(context = CommonPool) {.
    loadTweets("#AndroidMakers").
    }.
    launch(context = UI) {.
    showTweets(deferred.await()).
    }.
    deferred.cancel().
    Will throw CancellationException!

    View Slide

  36. // Launch a coroutine that returns a deferred value
    val deferred: Deferred> = async {
    loadTweets("#AndroidMakers")
    }
    // Fire and forget!
    val job: Job = launch { loadTweets("#AndroidMakers") }

    View Slide

  37. SUSPENDED FUNCTION
    interface Continuation {
    val context: CoroutineContext
    fun resume(value: T)
    fun resumeWithException(exception: Throwable)
    }

    View Slide

  38. SUSPENDED FUNCTION
    suspend fun CompletableFuture.await(): T =
    suspendCoroutine { cont: Continuation ->
    whenComplete { result, exception ->
    if (exception == null) // the future has been completed normally
    cont.resume(result)
    else // the future has completed with an exception
    cont.resumeWithException(exception)
    }
    }

    View Slide

  39. Pause d'eau…

    View Slide

  40. LET’S MAKE A DSL*!
    * Domain Specific Language

    View Slide

  41. fun Activity.load(loadFunction: () -> T) {.
    // TODO perform background loading here.
    }.

    View Slide

  42. fun Activity.load(loadFunction: () -> T): T {.
    return loadFunction().
    }.

    View Slide

  43. fun Activity.load(loadFunction: () -> T): Deferred {.
    return async { loadFunction() }.
    }.

    View Slide

  44. fun Activity.load(loadFunction: () -> T): Deferred {.
    return async { loadFunction() }.
    }.
    .
    fun Deferred.then(uiFunction: (T) -> Unit) {.
    launch(UI) { uiFunction([email protected]()) }.
    }.

    View Slide

  45. fun Activity.load(loadFunction: () -> T): Deferred {.
    return async { loadFunction() }.
    }.
    .
    infix fun Deferred.then(uiFunction: (T) -> Unit) {.
    launch(UI) { uiFunction([email protected]()) }.
    }.

    View Slide

  46. override fun onStart() {
    super.onStart()
    load {
    loadTweets("#AndroidMakers")
    } then {
    showTweets(it)
    }
    }
    ALMOST THERE!

    View Slide

  47. LIFECYCLE OBSERVER
    class CoroutineLifecycleListener : LifecycleObserver {.
    @OnLifecycleEvent(Lifecycle.Event.ON_STOP).
    fun onStop() {.
    // Called at onStop()
    }.
    }.

    View Slide

  48. LIFECYCLE OBSERVER
    class CoroutineLifecycleListener(val job: Job)
    : LifecycleObserver {
    @OnLifecycleEvent(Lifecycle.Event.ON_STOP)
    fun onStop() {
    if(!job.isCancelled) job.cancel()
    }
    }

    View Slide

  49. fun Activity.load(loadFunction: () -> T): Deferred {.
    return asynco{oloadFunction()o}o
    }.
    .
    infix fun Deferred.then(uiFunction: (T) -> Unit) {.
    launch(context = UI) {.
    uiFunction([email protected]()).
    }.
    }.

    View Slide

  50. fun LifecycleOwner.load(loadFunction: () -> T): Deferred {.
    val deferred = asynco{oloadFunction()o}o
    lifecycle.addObserver(CoroutineLifecycleListener(deferred)).
    return deferred.
    }.
    .
    infix fun Deferred.then(uiFunction: (T) -> Unit) {.
    launch(context = UI) {.
    uiFunction([email protected]()).
    }.
    }.

    View Slide

  51. val loaderContext: CoroutineContext = newFixedThreadPoolContext(2, "loader")
    fun LifecycleOwner.load(context: CoroutineContext = loaderContext,
    loadFunction: () -> T): Deferred {
    val deferred = async(context = context, start = CoroutineStart.LAZY) {
    loadFunction()
    }
    lifecycle.addObserver(CoroutineLifecycleListener(deferred))
    return deferred
    }
    infix fun Deferred.then(uiFunction: (T) -> Unit) {
    launch(context = UI) {
    uiFunction([email protected]())
    }
    }
    MORE CONTROL

    View Slide

  52. val loaderContext: CoroutineContext = newFixedThreadPoolContext(2, "loader")
    fun LifecycleOwner.load(context: CoroutineContext = loaderContext,
    loadFunction: () -> T): Deferred {
    val deferred = async(context = context, start = CoroutineStart.LAZY) {
    loadFunction()
    }
    lifecycle.addObserver(CoroutineLifecycleListener(deferred))
    return deferred
    }
    infix fun Deferred.then(uiFunction: (T) -> Unit) {
    launch(context = UI) {
    uiFunction([email protected]())
    }
    }
    MORE CONTROL

    View Slide

  53. val loaderContext: CoroutineContext = newFixedThreadPoolContext(2, "loader")
    fun LifecycleOwner.load(context: CoroutineContext = loaderContext,
    loadFunction: () -> T): Deferred {
    val deferred = async(context = context, start = CoroutineStart.LAZY) {
    loadFunction()
    }
    lifecycle.addObserver(CoroutineLifecycleListener(deferred))
    return deferred
    }
    infix fun Deferred.then(uiFunction: (T) -> Unit) {
    launch(context = UI) {
    uiFunction([email protected]())
    }
    }
    MORE CONTROL

    View Slide

  54. override fun onStart() {
    super.onStart()
    load {
    loadTweets("#AndroidMakers")
    } then {
    showTweets(it)
    }
    }
    DONE!

    View Slide

  55. WHAT ABOUT ERRORS?

    View Slide

  56. interface Continuation {
    val context: CoroutineContext
    fun resume(value: T)
    funoresumeWithException(exception: Throwable)
    }

    View Slide

  57. interface Continuation {
    val context: CoroutineContext
    fun resume(value: T)
    funoresumeWithException(exception: Throwable)
    }

    View Slide

  58. override fun onStart() {.
    super.onStart().
    .
    load.{.
    loadTweets("#AndroidMakers").
    }.then {.
    showTweets(it).
    }.
    }.

    View Slide

  59. override fun onStart() {.
    super.onStart().
    .
    load.{.
    loadTweets("#AndroidMakers").
    }.then {.
    try {.
    showTweets(it).
    } catch (e: Exception) {.
    showErrorMessage(e).
    }.
    }.
    }.

    View Slide

  60. BUT WAIT!
    THERE IS MORE!

    View Slide

  61. // Create a channel for reading and writing numbers
    val channel = Channel()
    // Send 10 integers to the channel on a background thread
    launch(CommonPool) { for (x in 1..10) channel.send(x) }
    // Read the numbers from the channel
    launch(UI) { for (number in channel) println(number) }
    CHANNELS!

    View Slide

  62. CAN BE USED LIKE A SEQUENCE!
    launch(UI) {
    channel.filter { it % 2 == 0 }
    .map(CommonPool) { it * 2 }
    .consumeEach { updateUI(it) }
    }
    Operators can take a coroutine context!

    View Slide

  63. CAN HAVE MULTIPLE CONSUMERS
    val clickChannel = Channel()
    button.setOnClickListener { view ->
    launch { clickChannel.send(view) }
    }
    repeat(10)_{_
    launch(CommonPool)_{_
    clickChannel.consumeEach_{_
    val result = doHeavyWork()_
    }_
    }_
    }_

    View Slide

  64. …OR MULTIPLE PRODUCERS
    val resultChannel = Channel()_
    _
    repeat(10)_{_
    launch(CommonPool)_{_
    clickChannel.consumeEach_{_
    val_result_=_doHeavyWork()_
    resultChannel.send(result)_
    }_
    }_
    }_
    _
    launch(UI)_{ resultChannel.consumeEach { updateUI(it) } }

    View Slide

  65. button.setOnClickListener {
    load {
    loadTweets(“#AndroidMakers")
    } then {
    showTweets(it)
    }
    }
    How can I throttle the click callbacks?
    Launches a new coroutine on every click!

    View Slide

  66. ACTORS

    View Slide

  67. val clickActor = actor(UI) {
    channel.map(CommonPool) { loadTweets("#AndroidMakers") }
    .consumeEach { showTweets(it) }
    }
    button.setOnClickListener { clickActor.offer(it) }
    ...
    clickActor.close()
    Drop this event if the receiver is busy!

    View Slide

  68. CONFUSED?

    View Slide

  69. LET’S EXTEND OUR DSL!

    View Slide

  70. class LoadingChannel(val lifecycle: Lifecycle,_
    val view: View,_
    val loadFunction: () -> T) {_
    _
    }_

    View Slide

  71. class LoadingChannel(val lifecycle: Lifecycle,_
    val view: View,_
    val loadFunction: () -> T) {_
    infix fun then(uiFunction: (T) -> Unit) {_
    val job = Job()
    val actor = actor(context = UI,
    parent = job) { }_
    _
    lifecycle.addObserver(CoroutineLifecycleListener(job))_
    _
    view.setOnClickListener { actor.offer(Unit) }_
    }_
    }_

    View Slide

  72. class LoadingChannel(val lifecycle: Lifecycle,_
    val view: View,_
    val loadFunction: () -> T) {_
    infix fun then(uiFunction: (T) -> Unit) {_
    val job = Job()
    val actor = actor(context = UI,
    parent = job) {
    channel.map(CommonPool) { loadFunction() }_
    .consumeEach { uiFunction(it) }_
    }_
    _
    lifecycle.addObserver(CoroutineLifecycleListener(job))_
    _
    view.setOnClickListener { actor.offer(Unit) }_
    }_
    }_

    View Slide

  73. class LoadingChannel(val lifecycle: Lifecycle,_
    val view: View,_
    val loadFunction: () -> T) {_
    infix fun then(uiFunction: (T) -> Unit) {_
    val job = Job()
    val actor = actor(context = UI,
    parent = job) {
    channel.map(CommonPool) { loadFunction() }_
    .consumeEach { uiFunction(it) }_
    }_
    _
    lifecycle.addObserver(ActorLifecycleListener(job))_
    _
    view.setOnClickListener { actor.offer(Unit) }_
    }_
    }_

    View Slide

  74. fun LifecycleOwner.whenClicking(view: View,
    loadFunction: () -> T) : LoadingChannel {
    return LoadingChannel(lifecycle, view, loadFunction)
    }

    View Slide

  75. override fun onCreate(savedInstanceState: Bundle?) {_
    super.onCreate(savedInstanceState)_
    setContentView(R.layout.activity_main)_
    _
    whenClicking(button) {_
    loadTweets("#AndroidMakers")_
    }_
    }_

    View Slide

  76. override fun onCreate(savedInstanceState: Bundle?) {_
    super.onCreate(savedInstanceState)_
    setContentView(R.layout.activity_main)_
    _
    whenClicking(button) {_
    loadTweets("#AndroidMakers")_
    }_then {_
    showTweets(it)_
    }_
    }_

    View Slide

  77. DOES THIS REPLACE RXJAVA?
    NO!

    View Slide

  78. COROUTINE LIBRARIES

    View Slide

  79. RETROFIT ADAPTER
    https://github.com/JakeWharton/retrofit2-kotlin-coroutines-adapter
    val retrofit = Retrofit.Builder()
    .baseUrl("https://example.com/")
    .addCallAdapterFactory(CoroutineCallAdapterFactory())
    .build()
    interface MyService {
    @GET("/user")
    fun getUser(): Deferred
    // or
    @GET("/user")
    fun getUser(): Deferred>
    }

    View Slide

  80. GRADLE STUFF
    kotlin {
    experimental {
    coroutines "enable"
    }
    }
    dependencies {
    implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.5"
    implementation “org.jetbrains.kotlinx:kotlinx-coroutines-android:0.22.5"
    }

    View Slide

  81. RESOURCES
    • https://github.com/Kotlin/kotlinx.coroutines/
    • https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md
    • https://github.com/Kotlin/kotlinx.coroutines/blob/master/ui/coroutines-guide-ui.md
    • https://github.com/JakeWharton/retrofit2-kotlin-coroutines-adapter
    • https://hellsoft.se/simple-asynchronous-loading-with-kotlin-coroutines-f26408f97f46
    • https://github.com/ErikHellman/KotlinAsyncWithCoroutines

    View Slide

  82. THANK YOU FOR LISTENING!
    https://speakerdeck.com/erikhellman/better-async-with-kotlin-coroutines

    View Slide