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.

2307a37297162f815342545a2068b2f1?s=128

Erik Hellman

April 20, 2018
Tweet

Transcript

  1. 1.

    BETTER ASYNC ON ANDROID WITH KOTLIN COROUTINES Erik Hellman -

    @ErikHellman https://speakerdeck.com/erikhellman/better-async-with-kotlin-coroutines
  2. 4.

    // This must run on a background thread @WorkerThread fun

    loadTweets(query: String): List<Tweet> { // Load tweets matching the search query } // This must run on the main thread @MainThread fun showTweets(tweets: List<Tweet>) { // Display tweets }
  3. 5.

    inner class LoadTweetAsyncTask : AsyncTask<String, Unit, List<Tweet>>() { override fun

    doInBackground(vararg params: String): List<Tweet> { return loadTweets(params[0]) } override fun onPostExecute(result: List<Tweet>?) { showTweets(result) } }
  4. 7.

    val disposable = Single_ .fromCallable { loadTweets("#AndroidMakers") }_ .subscribeOn(Schedulers.io())_ .observeOn(AndroidSchedulers.mainThread())_

    .subscribe(_ {_tweets -> showTweets(tweets)_}_ {_error -> showError(error)_})_ _ disposable.dispose()_
  5. 8.

    val disposable = Single_ .fromCallable { loadTweets("#AndroidMakers") }_ .subscribeOn(Schedulers.io())_ .observeOn(AndroidSchedulers.mainThread())_

    .subscribe(_ {_tweets -> showTweets(tweets)_}_ {_error -> showError(error)_})_ _ disposable.dispose()_
  6. 9.

    val disposable = Single_ .fromCallable { loadTweets("#AndroidMakers") }_ .subscribeOn(Schedulers.io())_ .observeOn(AndroidSchedulers.mainThread())_

    .subscribe(_ {_tweets -> showTweets(tweets)_}_ {_error -> showError(error)_})_ _ disposable.dispose()_
  7. 10.

    val disposable = Single_ .fromCallable { loadTweets("#AndroidMakers") }_ .subscribeOn(Schedulers.io())_ .observeOn(AndroidSchedulers.mainThread())_

    .subscribe(_ {_tweets -> showTweets(tweets)_}_ {_error -> showError(error)_})_ _ disposable.dispose()_
  8. 11.

    val disposable = Single_ .fromCallable { loadTweets("#AndroidMakers") }_ .subscribeOn(Schedulers.io())_ .observeOn(AndroidSchedulers.mainThread())_

    .subscribe(_ {_tweets -> showTweets(tweets)_}_ {_error -> showError(error)_})_ _ disposable.dispose()_
  9. 15.
  10. 17.
  11. 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 } }
  12. 28.

    val job = launch(context = CommonPool) {. val tweets =

    loadTweets("#AndroidMakers"). }. job.cancel().
  13. 29.

    val job = launch(context = CommonPool) {. val tweets =

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

    public actual fun launch(. context: CoroutineContext = DefaultDispatcher,. start: CoroutineStart

    = CoroutineStart.DEFAULT,. parent: Job? = null,. block: suspend CoroutineScope.() -> Unit. ): Job.
  15. 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
  16. 32.

    public actual fun launch( context: CoroutineContext = DefaultDispatcher, start: CoroutineStart

    = CoroutineStart.DEFAULT,. parent: Job? = null, block: suspend CoroutineScope.() -> Unit ): Job. START NOW OR LAZILY?
  17. 33.

    public actual fun launch( context: CoroutineContext = DefaultDispatcher, start: CoroutineStart

    = CoroutineStart.DEFAULT, parent: Job? = null, block: suspend CoroutineScope.() -> Unit ): Job SPECIFY A PARENT COROUTINE
  18. 34.

    public actual fun launch( context: CoroutineContext = DefaultDispatcher, start: CoroutineStart

    = CoroutineStart.DEFAULT, parent: Job? = null, block: suspend CoroutineScope.() -> Unit. ): Job THE FUNCTION TO RUN
  19. 35.

    val deferred = async(context = CommonPool) {. loadTweets("#AndroidMakers"). }. launch(context

    = UI) {. showTweets(deferred.await()). }. deferred.cancel(). Will throw CancellationException!
  20. 36.

    // Launch a coroutine that returns a deferred value val

    deferred: Deferred<List<Tweet>> = async { loadTweets("#AndroidMakers") } // Fire and forget! val job: Job = launch { loadTweets("#AndroidMakers") }
  21. 37.

    SUSPENDED FUNCTION interface Continuation<in T> { val context: CoroutineContext fun

    resume(value: T) fun resumeWithException(exception: Throwable) }
  22. 38.

    SUSPENDED FUNCTION suspend fun <T> CompletableFuture<T>.await(): T = suspendCoroutine<T> {

    cont: Continuation<T> -> 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) } }
  23. 44.

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

    { loadFunction() }. }. . fun <T> Deferred<T>.then(uiFunction: (T) -> Unit) {. launch(UI) { uiFunction(this@then.await()) }. }.
  24. 45.

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

    { loadFunction() }. }. . infix fun <T> Deferred<T>.then(uiFunction: (T) -> Unit) {. launch(UI) { uiFunction(this@then.await()) }. }.
  25. 49.

    fun <T> Activity.load(loadFunction: () -> T): Deferred<T> {. return asynco{oloadFunction()o}o

    }. . infix fun <T> Deferred<T>.then(uiFunction: (T) -> Unit) {. launch(context = UI) {. uiFunction(this@then.await()). }. }.
  26. 50.

    fun <T> LifecycleOwner.load(loadFunction: () -> T): Deferred<T> {. val deferred

    = asynco{oloadFunction()o}o lifecycle.addObserver(CoroutineLifecycleListener(deferred)). return deferred. }. . infix fun <T> Deferred<T>.then(uiFunction: (T) -> Unit) {. launch(context = UI) {. uiFunction(this@then.await()). }. }.
  27. 51.

    val loaderContext: CoroutineContext = newFixedThreadPoolContext(2, "loader") fun <T> LifecycleOwner.load(context: CoroutineContext

    = loaderContext, loadFunction: () -> T): Deferred<T> { val deferred = async(context = context, start = CoroutineStart.LAZY) { loadFunction() } lifecycle.addObserver(CoroutineLifecycleListener(deferred)) return deferred } infix fun <T> Deferred<T>.then(uiFunction: (T) -> Unit) { launch(context = UI) { uiFunction(this@then.await()) } } MORE CONTROL
  28. 52.

    val loaderContext: CoroutineContext = newFixedThreadPoolContext(2, "loader") fun <T> LifecycleOwner.load(context: CoroutineContext

    = loaderContext, loadFunction: () -> T): Deferred<T> { val deferred = async(context = context, start = CoroutineStart.LAZY) { loadFunction() } lifecycle.addObserver(CoroutineLifecycleListener(deferred)) return deferred } infix fun <T> Deferred<T>.then(uiFunction: (T) -> Unit) { launch(context = UI) { uiFunction(this@then.await()) } } MORE CONTROL
  29. 53.

    val loaderContext: CoroutineContext = newFixedThreadPoolContext(2, "loader") fun <T> LifecycleOwner.load(context: CoroutineContext

    = loaderContext, loadFunction: () -> T): Deferred<T> { val deferred = async(context = context, start = CoroutineStart.LAZY) { loadFunction() } lifecycle.addObserver(CoroutineLifecycleListener(deferred)) return deferred } infix fun <T> Deferred<T>.then(uiFunction: (T) -> Unit) { launch(context = UI) { uiFunction(this@then.await()) } } MORE CONTROL
  30. 56.
  31. 57.
  32. 59.

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

    try {. showTweets(it). } catch (e: Exception) {. showErrorMessage(e). }. }. }.
  33. 61.

    // Create a channel for reading and writing numbers val

    channel = Channel<Int>() // 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!
  34. 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!
  35. 63.

    CAN HAVE MULTIPLE CONSUMERS val clickChannel = Channel<View>() button.setOnClickListener {

    view -> launch { clickChannel.send(view) } } repeat(10)_{_ launch(CommonPool)_{_ clickChannel.consumeEach_{_ val result = doHeavyWork()_ }_ }_ }_
  36. 64.

    …OR MULTIPLE PRODUCERS val resultChannel = Channel<Int>()_ _ repeat(10)_{_ launch(CommonPool)_{_

    clickChannel.consumeEach_{_ val_result_=_doHeavyWork()_ resultChannel.send(result)_ }_ }_ }_ _ launch(UI)_{ resultChannel.consumeEach { updateUI(it) } }
  37. 65.

    button.setOnClickListener { load { loadTweets(“#AndroidMakers") } then { showTweets(it) }

    } How can I throttle the click callbacks? Launches a new coroutine on every click!
  38. 66.
  39. 67.

    val clickActor = actor<View>(UI) { channel.map(CommonPool) { loadTweets("#AndroidMakers") } .consumeEach

    { showTweets(it) } } button.setOnClickListener { clickActor.offer(it) } ... clickActor.close() Drop this event if the receiver is busy!
  40. 68.
  41. 71.

    class LoadingChannel<out T>(val lifecycle: Lifecycle,_ val view: View,_ val loadFunction:

    () -> T) {_ infix fun then(uiFunction: (T) -> Unit) {_ val job = Job() val actor = actor<Unit>(context = UI, parent = job) { }_ _ lifecycle.addObserver(CoroutineLifecycleListener(job))_ _ view.setOnClickListener { actor.offer(Unit) }_ }_ }_
  42. 72.

    class LoadingChannel<out T>(val lifecycle: Lifecycle,_ val view: View,_ val loadFunction:

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

    class LoadingChannel<out T>(val lifecycle: Lifecycle,_ val view: View,_ val loadFunction:

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

    fun <T> LifecycleOwner.whenClicking(view: View, loadFunction: () -> T) : LoadingChannel<T>

    { return LoadingChannel<T>(lifecycle, view, loadFunction) }
  45. 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" }