Slide 1

Slide 1 text

Kotlin Coroutines Beyond async/await Bolot Kerimbaev

Slide 2

Slide 2 text

Problem • We want async • Async is hard

Slide 3

Slide 3 text

Problem val image1 = loadImage(url1) val image2 = loadImage(url2) val result = composeImages(image1, image2) displayImage(result)

Slide 4

Slide 4 text

Solutions • AsyncTask • Callback • Observable, Reactive • Future, Promise

Slide 5

Slide 5 text

Callback loadImageCallback(url1) { image1 -> .loadImageCallback(url2) { image2 -> combineImages(image1, image2) { displayImage(result) } } }

Slide 6

Slide 6 text

Callback Hell peerConnection.setRemoteDescription(object: SdpObserver { override fun onSetFailure(error: String?) { } override fun onSetSuccess() { peerConnection.createAnswer(object : SdpObserver { override fun onSetFailure(error: String?) { } override fun onSetSuccess() { } override fun onCreateSuccess(sdp: SessionDescription?) { peerConnection.setLocalDescription(object : SdpObserver { override fun onSetFailure(error: String?) { } override fun onSetSuccess() { } override fun onCreateSuccess(sdp: SessionDescription?) { // TODO: get audio/video tracks } override fun onCreateFailure(error: String?) { } }, sdp) } override fun onCreateFailure(error: String?) { } }, mediaConstraints) } override fun onCreateSuccess(sdp: SessionDescription?) { } override fun onCreateFailure(error: String?) { } }, sessionDescription)

Slide 7

Slide 7 text

Rx loadImageRx(url1) .zipWith(loadImageRx(url2), ::combineImages) .subscribe()

Slide 8

Slide 8 text

Rx loadImageRx(url1) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .zipWith(loadImageRx(url2), ::combineImages) .subscribe()

Slide 9

Slide 9 text

Sequential val image1 = loadImage(url1) val image2 = loadImage(url2) val result = composeImages(image1, image2)

Slide 10

Slide 10 text

Coroutines val image1 = async { loadImage(url1) } val image2 = async { loadImage(url2) } val result = composeImages(image1.await(), image2.await())

Slide 11

Slide 11 text

async/await val image1 = async { loadImage(url1) } val image2 = async { loadImage(url2) } val result = composeImages(image1.await(), image2.await())

Slide 12

Slide 12 text

async/await

Slide 13

Slide 13 text

Behind the Scenes val image1 = loadImage(url1) val image2 = loadImage(url2) val result = composeImages(image1, image2) displayImage(result)

Slide 14

Slide 14 text

Behind the Scenes class CoroutinesReversed { class Continuation(val label: Int, val context: Map, val error: Throwable?) fun load(cont: Continuation) { when (cont.label) { 0 -> { cont.error?.let { throw it } val url1 = cont.context["url1"] as String val image1 = loadImage(url1) load(Continuation(1, cont.context.plus("image1" to image1), null)) } 1 -> { cont.error?.let { throw it } val url2 = cont.context["url2"] as String val image2 = loadImage(url2) load(Continuation(2, cont.context.plus("image2" to image2), null)) } 2 -> { cont.error?.let { throw it } val image1 = cont.context["image1"] as Image val image2 = cont.context["image2"] as Image val result = composeImages(image1, image2) load(Continuation(3, cont.context.plus("result" to result), null)) } 3 -> { val result = cont.context["result"] as Image displayImage(result) } } } fun load() = load(Continuation(0, emptyMap(), null)) }

Slide 15

Slide 15 text

Behind the Scenes class CoroutinesReversed { class Continuation(val label: Int, val context: Map, val error: Throwable?) fun load(cont: Continuation) { when (cont.label) { 0 -> { cont.error?.let { throw it } val url1 = cont.context["url1"] as String val image1 = loadImage(url1) load(Continuation(1, cont.context.plus("image1" to image1), null)) } 1 -> { cont.error?.let { throw it } val url2 = cont.context["url2"] as String val image2 = loadImage(url2) load(Continuation(2, cont.context.plus("image2" to image2), null)) } 2 -> { cont.error?.let { throw it } val image1 = cont.context["image1"] as Image val image2 = cont.context["image2"] as Image val result = composeImages(image1, image2) load(Continuation(3, cont.context.plus("result" to result), null)) } 3 -> { val result = cont.context["result"] as Image displayImage(result) } } } fun load() = load(Continuation(0, emptyMap(), null)) }

Slide 16

Slide 16 text

Behind the Scenes class CoroutinesReversed { class Continuation(val label: Int, val context: Map, val error: Throwable?) fun load(cont: Continuation) { when (cont.label) { 0 -> { cont.error?.let { throw it } val url1 = cont.context["url1"] as String val image1 = loadImage(url1) load(Continuation(1, cont.context.plus("image1" to image1), null)) } 1 -> { cont.error?.let { throw it } val url2 = cont.context["url2"] as String val image2 = loadImage(url2) load(Continuation(2, cont.context.plus("image2" to image2), null)) } 2 -> { cont.error?.let { throw it } val image1 = cont.context["image1"] as Image val image2 = cont.context["image2"] as Image val result = composeImages(image1, image2) load(Continuation(3, cont.context.plus("result" to result), null)) } 3 -> { val result = cont.context["result"] as Image displayImage(result) } } } fun load() = load(Continuation(0, emptyMap(), null)) }

Slide 17

Slide 17 text

Behind the Scenes class Continuation(val label: Int, val context: Map, val error: Throwable?) 2 -> { cont.error?.let { throw it } val image1 = cont.context["image1"] as Image val image2 = cont.context["image2"] as Image val result = composeImages(image1, image2) load(Continuation(3, cont.context.plus("result" to result), null)) }

Slide 18

Slide 18 text

Behind the Scenes fun load() = load(Continuation(0, emptyMap(), null))

Slide 19

Slide 19 text

Behind the Scenes class CoroutinesReversed { class Continuation(val label: Int, val context: Map, val error: Throwable?) fun load(cont: Continuation) { when (cont.label) { 0 -> { cont.error?.let { throw it } val url1 = cont.context["url1"] as String val image = loadImage(url1) load(Continuation(1, cont.context.plus("image1" to image), null)) } 1 -> { cont.error?.let { throw it } val url2 = cont.context["url2"] as String val image = loadImage(url2) load(Continuation(2, cont.context.plus("image2" to image), null)) } 2 -> { cont.error?.let { throw it } val image1 = cont.context["image1"] as Image val image2 = cont.context["image2"] as Image val result = composeImages(image1, image2) load(Continuation(3, cont.context.plus("result" to result), null)) } 3 -> { val result = cont.context["result"] as Image displayImage(result) } } } fun load() = load(Continuation(0, emptyMap(), null)) }

Slide 20

Slide 20 text

Continuation public interface Continuation { public val context: CoroutineContext public fun resume(value: T) public fun resumeWithException(exception: Throwable) }

Slide 21

Slide 21 text

Retrofit suspend fun Call.await(): T { return suspendCoroutine { cont -> enqueue(object: Callback { override fun onResponse(call: Call?, response: Response) { if (response.isSuccessful) { val body = response.body() ?: return cont.resumeWithException(NullPointerException()) cont.resume(body) } else { cont.resumeWithException(ApiError()) } } override fun onFailure(call: Call?, t: Throwable) { cont.resumeWithException(t) } }) } }

Slide 22

Slide 22 text

Retrofit suspend fun Call.await(): T { return suspendCoroutine { cont -> enqueue(object: Callback { override fun onResponse(call: Call?, response: Response) { if (response.isSuccessful) { val body = response.body() ?: return cont.resumeWithException(NullPointerException()) cont.resume(body) } else { cont.resumeWithException(ApiError()) } } override fun onFailure(call: Call?, t: Throwable) { cont.resumeWithException(t) } }) } }

Slide 23

Slide 23 text

Retrofit dependencies { implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines- experimental-adapter:1.0.0' } Retrofit.Builder() .baseUrl(baseUrl) .client(client) .addConverterFactory(gsonConverterFactory) .addCallAdapterFactory(CoroutineCallAdapterFactory()) .build()

Slide 24

Slide 24 text

Retrofit interface ApiService { @GET fun loadTweets(): Call> }

Slide 25

Slide 25 text

Retrofit interface ApiService { @GET fun loadTweets(): Call> }

Slide 26

Slide 26 text

Retrofit interface ApiService { @GET fun loadTweets(): Deferred> }

Slide 27

Slide 27 text

Retrofit interface ApiService { @GET fun loadTweets(): Deferred> }

Slide 28

Slide 28 text

Retrofit interface ApiService { @GET fun loadTweets(): Deferred> } launch { val tweets = service.loadTweets() displayTweets(tweets.await()) }

Slide 29

Slide 29 text

Retrofit interface ApiService { @GET fun loadTweets(): Deferred> }

Slide 30

Slide 30 text

Retrofit interface ApiService { @GET fun loadTweets(): Deferred>> }

Slide 31

Slide 31 text

Retrofit interface ApiService { @GET fun loadTweets(): Deferred>> } launch { val deferred = service3.loadTweets() val response = deferred.await() if (response.isSuccessful) { response.body()?.let { displayTweets(it) } } }

Slide 32

Slide 32 text

Retrofit interface ApiService { @GET fun loadTweets(): Deferred>> } launch { val deferred = service3.loadTweets() val response = deferred.await() if (response.isSuccessful) { response.body()?.let { displayTweets(it) } } }

Slide 33

Slide 33 text

async public fun async( context: CoroutineContext = DefaultDispatcher, start: CoroutineStart = CoroutineStart.DEFAULT, parent: Job? = null, onCompletion: CompletionHandler? = null, block: suspend CoroutineScope.() -> T ): Deferred

Slide 34

Slide 34 text

async public fun async( context: CoroutineContext = DefaultDispatcher, start: CoroutineStart = CoroutineStart.DEFAULT, parent: Job? = null, onCompletion: CompletionHandler? = null, block: suspend CoroutineScope.() -> T ): Deferred

Slide 35

Slide 35 text

await override suspend fun await(): T = awaitInternal() as T

Slide 36

Slide 36 text

Coroutine Builders val job = launch { } val deferred = async { } val result = runBlocking { }

Slide 37

Slide 37 text

launch/join val jobs = List(100_000) { launch { delay(5000) print(".") } } jobs.forEach { it.join() }

Slide 38

Slide 38 text

launch/join val jobs = List(100_000) { launch(context = CommonPool) { delay(5000) print(".") } } jobs.forEach { it.join() }

Slide 39

Slide 39 text

launch/join val jobs = List(100_000) { launch(context = CommonPool) { delay(5000) print(".") } } jobs.forEach { it.join() }

Slide 40

Slide 40 text

launch/join val jobs = List(100_000) { launch(context = CommonPool) { delay(5000) print(".") } } jobs.forEach { it.join() }

Slide 41

Slide 41 text

launch/join val jobs = List(100_000) { launch(CommonPool) { delay(5000) print(".") } } jobs.forEach { it.join() }

Slide 42

Slide 42 text

Coroutine Context val image1 = async { loadImage(url1) } val image2 = async { loadImage(url2) } val result = composeImages(image1.await(), image2.await())

Slide 43

Slide 43 text

Coroutine Context public interface CoroutineContext { public operator fun get(key: Key): E? public fun fold(initial: R, operation: (R, Element) -> R): R public operator fun plus(context: CoroutineContext): CoroutineContext public fun minusKey(key: Key<*>): CoroutineContext public interface Element : CoroutineContext public interface Key }

Slide 44

Slide 44 text

Coroutine Context log("Thread: ${Thread.currentThread().name}") log("Context: $coroutineContext") log("Job: ${coroutineContext[Job]}") log("Interceptor: ${coroutineContext[ContinuationInterceptor]}")

Slide 45

Slide 45 text

Coroutine Context Thread: ForkJoinPool.commonPool-worker-1
 Context: [DeferredCoroutine{Active}@424f90d, CommonPool]
 Job: DeferredCoroutine{Active}@424f90d
 Interceptor: CommonPool Thread: ForkJoinPool.commonPool-worker-2
 Context: [DeferredCoroutine{Active}@4ae30c2, CommonPool]
 Job: DeferredCoroutine{Active}@4ae30c2
 Interceptor: CommonPool

Slide 46

Slide 46 text

Retry • If at first you don’t succeed… • …Redefine success? • …Retry?

Slide 47

Slide 47 text

Retry val image1 = loadImage(url1) val image2 = loadImage(url1) composeImages(image1, image2)

Slide 48

Slide 48 text

Retry val image1 = retryIO { loadImage(url1) } val image2 = retryIO(times = 3) { loadImage(url1) } composeImages(image1, image2)

Slide 49

Slide 49 text

Retry val image1 = retryIO { loadImage(url1) } val image2 = retryIO(times = 3) { loadImage(url1) } composeImages(image1, image2)

Slide 50

Slide 50 text

retryIO suspend fun retryIO( times: Int = Int.MAX_VALUE, initialDelay: Long = 100, // 0.1 second maxDelay: Long = 1000, // 1 second factor: Double = 2.0, block: suspend () -> T): T { var currentDelay = initialDelay repeat(times - 1) { try { return block() } catch (e: IOException) { Timber.e(e, "retryIO") } delay(currentDelay) currentDelay = (currentDelay * factor).toLong().coerceAtMost(maxDelay) } return block() // last attempt }

Slide 51

Slide 51 text

Lifecycle var job: Job? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) job = launch(UI) { // ... } } override fun onDestroy() { super.onDestroy() job?.cancel() }

Slide 52

Slide 52 text

Lifecycle var job: Job? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) job = launch(UI) { // ... } } override fun onDestroy() { super.onDestroy() job?.cancel() }

Slide 53

Slide 53 text

Channels val channel = Channel() launch(CommonPool) { repeat(42) { channel.send(it)} } launch(UI) { for (i in channel) log("$i") }

Slide 54

Slide 54 text

Channels public interface Channel : SendChannel, ReceiveChannel public interface SendChannel { public suspend fun send(element: E) public fun close(cause: Throwable? = null): Boolean } public interface ReceiveChannel { public suspend fun receive(): E public operator fun iterator(): ChannelIterator }

Slide 55

Slide 55 text

Channels data class ActivityResult(val requestCode: Int, val resultCode: Int, val data: Intent?) private val activityResultChannel = Channel() override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { launch { activityResultChannel.send(ActivityResult(requestCode, resultCode, data)) } } suspend fun startActivityForResultAsync(intent: Intent, requestCode: Int): ActivityResult { startActivityForResult(intent, requestCode) return activityResultChannel.receive() } launch { val result = startActivityForResultAsync(intent, 3) }

Slide 56

Slide 56 text

Channels data class ActivityResult(val requestCode: Int, val resultCode: Int, val data: Intent?) private val activityResultChannel = Channel() override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { launch { activityResultChannel.send(ActivityResult(requestCode, resultCode, data)) } } suspend fun startActivityForResultAsync(intent: Intent, requestCode: Int): ActivityResult { startActivityForResult(intent, requestCode) return activityResultChannel.receive() } launch { val result = startActivityForResultAsync(intent, 3) }

Slide 57

Slide 57 text

Channels data class ActivityResult(val requestCode: Int, val resultCode: Int, val data: Intent?) private val activityResultChannel = Channel() override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { launch { activityResultChannel.send(ActivityResult(requestCode, resultCode, data)) } } suspend fun startActivityForResultAsync(intent: Intent, requestCode: Int): ActivityResult { startActivityForResult(intent, requestCode) return activityResultChannel.receive() } launch { val result = startActivityForResultAsync(intent, 3) }

Slide 58

Slide 58 text

Channels data class ActivityResult(val requestCode: Int, val resultCode: Int, val data: Intent?) private val activityResultChannel = Channel() override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { launch { activityResultChannel.send(ActivityResult(requestCode, resultCode, data)) } } suspend fun startActivityForResultAsync(intent: Intent, requestCode: Int): ActivityResult { startActivityForResult(intent, requestCode) return activityResultChannel.receive() } launch { val result = startActivityForResultAsync(intent, 3) }

Slide 59

Slide 59 text

Channels data class ActivityResult(val requestCode: Int, val resultCode: Int, val data: Intent?) private val activityResultChannel = Channel() override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { launch { activityResultChannel.send(ActivityResult(requestCode, resultCode, data)) } } suspend fun startActivityForResultAsync(intent: Intent, requestCode: Int): ActivityResult { startActivityForResult(intent, requestCode) return activityResultChannel.receive() } launch { val result = startActivityForResultAsync(intent, 3) }

Slide 60

Slide 60 text

Channels data class ActivityResult(val requestCode: Int, val resultCode: Int, val data: Intent?) private val activityResultChannel = Channel() override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { launch { activityResultChannel.send(ActivityResult(requestCode, resultCode, data)) } } suspend fun startActivityForResultAsync(intent: Intent, requestCode: Int): ActivityResult { startActivityForResult(intent, requestCode) return activityResultChannel.receive() } launch { val result = startActivityForResultAsync(intent, 3) }

Slide 61

Slide 61 text

Channels val channel: Channel = Channel() sendButton.setOnClickListener { launch(UI) { val text = usernameEditText.text.toString() channel.send(text) } }

Slide 62

Slide 62 text

Channels launch(UI) { for (string in channel) { resultTextView.text = string } }

Slide 63

Slide 63 text

Channels var job: Job? = null override fun onViewCreated(view: View, savedInstanceState: Bundle?) { job = launch(UI) { for (string in channel) { resultTextView.text = channel.receive() } } } override fun onDestroyView() { super.onDestroyView() job?.cancel() }

Slide 64

Slide 64 text

More Coroutines • Actors • Mutexes • Sequences

Slide 65

Slide 65 text

Sequences val fibonacci = buildSequence { yield(1) var cur = 1 var next = 1 while(true) { yield(next) val tmp = cur + next cur = next next = tmp } } fibonacci.take(5).map { it*it }

Slide 66

Slide 66 text

Kotlin 1.3 • No longer “experimental” • buildSequence and buildIterator moved to the kotlin.sequences package • Simplified Continuation, only one method resumeWith(result:SuccessOrFailure) • KT-16908 Support callable references to suspending functions • KT-18559 Serializability of all coroutine-related classes