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

Kotlin Coroutines in Android

Kotlin Coroutines in Android

Atlanta Android Club version, March 2018

Charles Julian Knight

March 21, 2018
Tweet

More Decks by Charles Julian Knight

Other Decks in Technology

Transcript

  1. We’ve gotten okay at this... - Multithreading - AsyncTask -

    Callbacks - Promises / Futures - Observables / Reactive Streams
  2. Simple Flow Example error Load Friends from Network Ask for

    Permissions Contact permissions? Loading Show Retry Dialog retry? yes no Network Error success Canceled Granted? Read Contacts Show Results yes no yes no
  3. We’ve gotten okay at this... - Chaining Steps - Hard

    to read and maintain - 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
  4. “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)
  5. Setup dependencies { compile 'org.jetbrains.kotlinx:kotlinx-coroutines-core:0.22.5' compile 'org.jetbrains.kotlinx:kotlinx-coroutines-android:0.22.5' // RxJava 1

    compile 'org.jetbrains.kotlinx:kotlinx-coroutines-rx1:0.22.5' // RxJava 2 compile 'org.jetbrains.kotlinx:kotlinx-coroutines-rx2:0.22.5' // Java 8 CompletableFuture (minSdk 24) compile 'org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:0.22.5' // Java NIO (minSdk 26) compile 'org.jetbrains.kotlinx:kotlinx-coroutines-nio:0.22.5' } kotlin { experimental { coroutines 'enable' } }
  6. 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 } }
  7. 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) } }
  8. Coroutine Builders val job = launch(UI) { example() } job.isActive

    // true launch(CommonPool) { delay(100) print("ok") }
  9. 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")
  10. Concurrency - Async/Await val one = async(CommonPool) { delay(100) return@async

    1 } // one is a Promise (Deferred<Int>). 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()
  11. 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;
  12. Wrapping Callback APIs suspend fun <T> Task<T>.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() }
  13. - Single.await() - Observable.awaitFirst() - Observable.awaitFirstOrDefault() - Observable.awaitSingle() - Observable.awaitLast()

    - Observable.consumeEach { } Errors and cancellation work too. Convert Observables to Coroutines
  14. Coroutine Dialogs suspend fun showConfirmationDialog() = suspendCancellableCoroutine<Boolean> { 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() } }
  15. 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<ActivityResult> 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() } }
  16. 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()
  17. 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() } }
  18. Race Conditions, etc. var count = 500 (0..5).forEach { launch(CommonPool)

    { while(count > 0) { print(count) count = count - 1 delay(10) } } }
  19. Need to be Cancelled override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)

    launch(UI) { var timer = 0 while (true) { textView.text = "$timer seconds" delay(1, TimeUnit.SECONDS) timer++ } } } // will throw IllegalStateException: Activity has been destroyed
  20. Need to be Cancelled lateinit var job: Job override fun

    onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) job = launch(UI) { var timer = 0 while (true) { textView.text = "$timer seconds" delay(1, TimeUnit.SECONDS) timer++ } } } override fun onDestroy() { job.cancel() super.onDestroy() }
  21. Cancellation is an Exception val job = launch(UI) { try

    { dangerousMethod() // throws CancellationException } catch (e: Exception) { // CancellationException got gobbled up! Log.w("EXAMPLE", "dangerous method exploded") } doTheNextThing() // next thing still runs! } job.cancel()
  22. Cancellation is an Exception val job = launch(UI) { try

    { dangerousMethod() // throws CancellationException } catch (e: CancellationException){ throw e // rethrow, to be handled by the coroutine context } catch (e: Exception) { Log.w("EXAMPLE", "dangerous method exploded") } doTheNextThing() // next thing won't get called anymore } job.cancel()
  23. Cancellation is Cooperative // Not Cancellable! suspend fun <T> Task<T>.await():

    T = suspendCoroutine { cont -> addOnCompleteListener { cont.resume(it.result) } addOnFailureListener { cont.resumeWithException(it) } } // Not Cancellable! suspend fun readFromInputStream(inputStream: InputStream) { InputStreamReader(inputStream).forEachLine { line -> print(line) } }
  24. 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