Slide 1

Slide 1 text

Kotlin Coroutines in Android Untangle your async code @charlesjuliank Atlanta Android Club 2018

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 www.fixdapp.com

Slide 4

Slide 4 text

Outline - Why - Intro to Coroutines - Android-Specific Patterns - Gotchas

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Some Examples

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

Introducing Coroutines

Slide 11

Slide 11 text

Coroutines Simplify Async

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 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 14

Slide 14 text

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

Slide 15

Slide 15 text

github.com/KeepSafe/dexcount-gradle-plugin

Slide 16

Slide 16 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 17

Slide 17 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 18

Slide 18 text

Calling Coroutines

Slide 19

Slide 19 text

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

Slide 20

Slide 20 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 21

Slide 21 text

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

Slide 22

Slide 22 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 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 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 26

Slide 26 text

Usage in Android

Slide 27

Slide 27 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 28

Slide 28 text

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

Slide 29

Slide 29 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 30

Slide 30 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 31

Slide 31 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 32

Slide 32 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 33

Slide 33 text

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

Slide 34

Slide 34 text

Some Pitfalls

Slide 35

Slide 35 text

Race Conditions, etc. var count = 500 (0..5).forEach { launch(CommonPool) { while(count > 0) { print(count) count = count - 1 delay(10) } } }

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

Cancellation is Cooperative // Not Cancellable! suspend fun Task.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) } }

Slide 41

Slide 41 text

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

Slide 42

Slide 42 text

Growing the Team [email protected]

Slide 43

Slide 43 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