Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

About Us fixdapp.com

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Some Examples

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

Introducing Coroutines

Slide 16

Slide 16 text

Coroutines Simplify Async

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

“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)

Slide 19

Slide 19 text

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' } }

Slide 20

Slide 20 text

github.com/KeepSafe/dexcount-gradle-plugin

Slide 21

Slide 21 text

github.com/KeepSafe/dexcount-gradle-plugin

Slide 22

Slide 22 text

github.com/KeepSafe/dexcount-gradle-plugin

Slide 23

Slide 23 text

github.com/KeepSafe/dexcount-gradle-plugin

Slide 24

Slide 24 text

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 } }

Slide 25

Slide 25 text

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) } }

Slide 26

Slide 26 text

Calling Coroutines

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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")

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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()

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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;

Slide 34

Slide 34 text

Usage in Android

Slide 35

Slide 35 text

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() }

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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() } }

Slide 38

Slide 38 text

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() } }

Slide 39

Slide 39 text

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()

Slide 40

Slide 40 text

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() } }

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

Growing the Team [email protected]

Slide 44

Slide 44 text

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