$30 off During Our Annual Pro Sale. View Details »

Kotlin Coroutines in Android

Kotlin Coroutines in Android

Charles Julian Knight

September 22, 2017
Tweet

More Decks by Charles Julian Knight

Other Decks in Technology

Transcript

  1. Kotlin Coroutines in Android
    Untangle your async code
    @charlesjuliank
    Connect.Tech 2017

    View Slide

  2. About Me
    Charles Julian Knight
    he/him (or they/them)
    @charlesjuliank
    [email protected]

    View Slide

  3. About Us
    fixdapp.com

    View Slide

  4. Outline
    - The Problem
    - Existing Solutions
    - Intro to Coroutines
    - Android-Specific Patterns

    View Slide

  5. I/O is Hard
    Everything is I/O
    &
    Problem

    View Slide

  6. We’ve gotten pretty good at this...
    - AsyncTask
    - Callbacks
    - Promises / Futures
    - Observables / Reactive Streams

    View Slide

  7. Simple Flow Example
    Show Confirm Dialog
    Append Result
    Network Error
    error
    Canceled
    confirmed?
    Load Words from Network
    Count usage of word in file
    For each
    yes
    no
    Loading

    View Slide

  8. Some Examples

    View Slide

  9. We’ve gotten pretty good at this...
    - AsyncTask
    - Callbacks
    - Promises / Futures
    - Observables / Reactive Streams

    View Slide

  10. We’ve gotten pretty good at this...
    - AsyncTask
    - Boilerplate, blocking only, hard to chain
    - Callbacks
    - Promises / Futures
    - Observables / Reactive Streams

    View Slide

  11. We’ve gotten pretty good at this...
    - AsyncTask
    - Boilerplate, blocking only, hard to chain
    - Callbacks
    - Callback hell, hard to refactor, no cancellation
    - Promises / Futures
    - Observables / Reactive Streams

    View Slide

  12. We’ve gotten pretty good at this...
    - AsyncTask
    - Boilerplate, blocking only, hard to chain
    - Callbacks
    - Callback hell, hard to refactor, no cancellation
    - Promises / Futures
    - CompletableFuture minSdk 24, no cancellation, concurrent by default
    - Observables / Reactive Streams

    View Slide

  13. We’ve gotten pretty good at this...
    - AsyncTask
    - Boilerplate, blocking only, hard to chain
    - Callbacks
    - Callback hell, hard to refactor, no cancellation
    - Promises / Futures
    - CompletableFuture minSdk 24, no cancellation, concurrent by default
    - Observables / Reactive Streams
    - Steep learning curve, different paradigm, too many operators

    View Slide

  14. View Slide

  15. Introducing Coroutines

    View Slide

  16. Coroutines Simplify Async

    View Slide

  17. View Slide

  18. “Experimental”
    - Stable (as in not-buggy)
    - API is not finalized
    - Guaranteed backwards compatibility
    - Calls covered here are unlikely to change
    - IDE tools to ease migration
    Can I use it in production?
    “Yes! You should.”
    - Roman Elizarov (seconded by me)

    View Slide

  19. Setup
    dependencies {
    compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.18'
    compile 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.18'
    // RxJava 1
    compile 'org.jetbrains.kotlinx:kotlinx-coroutines-rx1:0.18'
    // RxJava 2
    compile 'org.jetbrains.kotlinx:kotlinx-coroutines-rx2:0.18'
    // Java 8 CompletableFuture (minSdk 24)
    compile 'org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:0.18'
    // Java NIO (minSdk 26)
    compile 'org.jetbrains.kotlinx:kotlinx-coroutines-nio:0.18'
    }
    kotlin { experimental { coroutines 'enable' } }

    View Slide

  20. github.com/KeepSafe/dexcount-gradle-plugin

    View Slide

  21. github.com/KeepSafe/dexcount-gradle-plugin

    View Slide

  22. github.com/KeepSafe/dexcount-gradle-plugin

    View Slide

  23. github.com/KeepSafe/dexcount-gradle-plugin

    View Slide

  24. Creating Coroutines
    suspend fun delayedParseInt(i: String): Int {
    return suspendCoroutine { cont ->
    // this code gets executed immediately to set up the coroutine
    Thread {
    Thread.sleep(1000)
    // some time later, use the continuation to resume from suspension
    try {
    val result = i.toInt()
    cont.resume(result)
    }catch (e: NumberFormatException) {
    cont.resumeWithException(e)
    }
    }.start()
    // after leaving this method, the caller will suspend until resume is called
    }
    }

    View Slide

  25. Combining Coroutines
    suspend fun doFirst(): String
    suspend fun doSecond(): Boolean
    suspend fun doThird(data: String)
    suspend fun example() {
    val data = doFirst()
    if (doSecond()) {
    doThird(data)
    }
    }

    View Slide

  26. Calling Coroutines

    View Slide

  27. Coroutine Builders
    val job = launch(UI) {
    example()
    }
    job.isActive // true
    launch(CommonPool) {
    delay(100)
    print("ok")
    }

    View Slide

  28. Coroutine Context
    Where the non-suspending code is run.
    Or another way:
    What thread picks back up after a suspension resumes.
    - UI
    - CommonPool
    - newSingleThreadContext(name = "single")
    - newFixedThreadPoolContext(nThreads = 3, name = "pool")

    View Slide

  29. Concurrency
    In Kotlin, coroutines are sequential by default.
    - Concurrency is hard
    - Most common use case is sequential

    View Slide

  30. Concurrency - Async/Await
    val one = async(CommonPool) {
    delay(100)
    return@async 1
    }
    // one is a Promise (Deferred). It has already started
    val two = async(CommonPool) {
    delay(100)
    return@async 2
    }
    // now both promises are running
    // await() suspends until the promise resolves
    val sum = one.await() + two.await()

    View Slide

  31. “Lightweight Threads”
    val jobs = List(100_000) {
    launch(CommonPool) {
    delay(1000)
    print(".")
    }
    }
    jobs.forEach { it.join() }

    View Slide

  32. Cancellation
    val job = launch(UI) {
    delay(1000)
    print("this never gets called")
    }
    Thread.sleep(500)
    job.cancel()

    View Slide

  33. Under the Hood
    void example(Continuation cont) {
    switch (cont.label) {
    case 0:
    cont.label = 1;
    showConfirmDialog(cont);
    break;
    case 1:
    if (cont.lastError != null)
    throw cont.lastError;
    boolean isConfirmed = (boolean) cont.lastValue;
    if (!isConfirmed) {
    textView.setText("cancelled");
    return;
    }
    textView.setText("loading");
    makeNetworkRequest(cont);
    break;
    case 2:
    if (cont.lastError != null)
    throw cont.lastError;

    View Slide

  34. Usage in Android

    View Slide

  35. Wrapping Callback APIs
    suspend fun Task.await(): T = suspendCoroutine { cont ->
    addOnCompleteListener { cont.resume(it.result) }
    addOnFailureListener { cont.resumeWithException(it) }
    }
    launch(UI) {
    val locationClient =
    LocationServices.getFusedLocationProviderClient(this)
    val location = locationClient.lastLocation.await()
    }

    View Slide

  36. - Single.await()
    - Observable.awaitFirst()
    - Observable.awaitFirstOrDefault()
    - Observable.awaitSingle()
    - Observable.awaitLast()
    - Observable.consumeEach { }
    Errors and cancellation work too.
    Convert Observables to Coroutines

    View Slide

  37. Coroutine Dialogs
    suspend fun showConfirmationDialog() = suspendCancellableCoroutine { cont ->
    val dialog = AlertDialog.Builder(this)
    .setMessage("Are you sure?")
    .setPositiveButton("Yes", { _, _ -> cont.resume(true) })
    .setNegativeButton("No", { _, _ -> cont.resume(false) })
    .setCancelable(true)
    .setOnCancelListener { cont.cancel() }
    .create()
    dialog.show()
    cont.invokeOnCompletion { if(cont.isCancelled) dialog.dismiss() }
    }

    View Slide

  38. Coroutine Activity Results
    interface ActivityResultMixin {
    data class ActivityResult(val requestCode: Int, val resultCode: Int, val data: Intent?)
    fun startActivityForResult(intent: Intent, requestCode: Int)
    val _activityResultStream: PublishSubject
    suspend fun startActivityForResultAsync(intent: Intent, requestCode: Int): ActivityResult {
    startActivityForResult(intent, requestCode)
    return _activityResultStream
    .filter { it.requestCode == requestCode }
    .awaitFirstOrDefault((ActivityResult(requestCode, Activity.RESULT_CANCELED, null)))
    }
    fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    _activityResultStream.onNext(ActivityResult(requestCode, resultCode, data))
    }
    fun onDestroy() {
    _activityResultStream.onCompleted()
    }
    }

    View Slide

  39. Blocking Code
    suspend fun saveUser(db: SQLiteDatabase, user: User) = async(CommonPool) {
    db.insert("users", null, ContentValues().apply {
    put("id", user.id)
    put("name", user.name)
    put("email", user.email)
    })
    }.await()

    View Slide

  40. Job Lifecycle
    class JobCancellationActivity : Activity() {
    var job: Job? = null
    override fun onStart() {
    super.onStart()
    job = launch(UI) { /* ... */ }
    }
    override fun onStop() {
    job?.cancel(CancellationException("onStop"))
    super.onStop()
    }
    }

    View Slide

  41. Testing Coroutines
    @Test
    fun asyncAddition_isCorrect() = runBlocking {
    val result = asyncAdd(2, 2)
    expect(result).to.equal(4)
    }

    View Slide

  42. There’s Much More
    - Channels
    - Generators
    - Actors
    - Mutexes (suspending locks)

    View Slide

  43. Growing the Team
    [email protected]

    View Slide

  44. Resources
    - Kotlin Coroutines Guide:
    github.com/Kotlin/kotlinx.coroutines
    - Introduction to Kotlin coroutines - Roman Elizarov (GeekOut 2017)
    vimeo.com/222499934
    - Examples from this talk:
    github.com/rabidaudio/kotlin-coroutines-android

    View Slide