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

Coroutines Party Tricks

Coroutines Party Tricks

Video: (coming soon)

Coroutines are an important tool in the Android developer’s toolbox. We use 'em to background our tasks, to parallelize our work, and to gracefully cancel operations.

This is not a talk about that reasonable coroutines use.

Instead, we’ll do unreasonable, forbidden things. We’ll study the implementation details of coroutines, scopes, and contexts. Once that foundation is established, we’ll start ‘thinking in coroutines’ and break our brains a bit!

We’ll cover:

💣 Making I/O slower with `suspend`
💣 Turning code inside-out
💣 Using reflection with coroutines
💣 Treating the user as a function

If you're willing to suspend disbelief, this talk is for you.

Avatar for Jesse Wilson

Jesse Wilson

June 25, 2025
Tweet

More Decks by Jesse Wilson

Other Decks in Technology

Transcript

  1. Party Tricks? Coroutines are neat You can build powerful things

    with ’em You can chop your head off too I’ve got 10 party tricks!
  2. A Seductive Idea I/O is a frequent source of blocking

    Blocking is bad Suspending is good Let’s do suspending I/O !
  3. Let’s Try Change Okio & Moshi to suspend all the

    functions that could block ~300 functions in Okio ~200 functions in Moshi
  4. Benchmark https://zacsweers.github.io/json-serialization-benchmarking/ @Benchmark fun blocking() { runBlocking { val jsonReader

    = RegularJsonReader.of(regularJson.clone()) jsonReader.readJsonValue() } } @Benchmark fun suspending() { runBlocking { val jsonReader = SuspendingJsonReader.of(suspendingJson.clone()) jsonReader.readJsonValue() } }
  5. Benchmark https://zacsweers.github.io/json-serialization-benchmarking/ @Benchmark fun blocking() { runBlocking { val jsonReader

    = RegularJsonReader.of(regularJson.clone()) jsonReader.readJsonValue() } } @Benchmark fun suspending() { runBlocking { val jsonReader = SuspendingJsonReader.of(suspendingJson.clone()) jsonReader.readJsonValue() } }
  6. Benchmark https://zacsweers.github.io/json-serialization-benchmarking/ @Benchmark fun blocking() { runBlocking { val jsonReader

    = RegularJsonReader.of(regularJson.clone()) jsonReader.readJsonValue() } } @Benchmark fun suspending() { runBlocking { val jsonReader = SuspendingJsonReader.of(suspendingJson.clone()) jsonReader.readJsonValue() } }
  7. class RealBufferedSource : BufferedSource { override fun require(byteCount: Long) {

    if (!request(byteCount)) { throw EOFException() } } override fun request(byteCount: Long): Boolean { ... } ... }
  8. class RealBufferedSource : BufferedSource { override fun require(byteCount: Long) {

    if (!request(byteCount)) { throw EOFException() } } override fun request(byteCount: Long): Boolean { ... } ... }
  9. class RealBufferedSource : BufferedSource { override suspend fun require(byteCount: Long)

    { if (!request(byteCount)) { throw EOFException() } } override suspend fun request(byteCount: Long): Boolean { ... } ... }
  10. fun requireSuspending( byteCount: Long, completion: Continuation<Any?>, ): Any? { class

    RequireContinuation(completion: Continuation<Any?>) : ContinuationImpl(completion) { var result: Any? = null var label: Int = 0 public override fun invokeSuspend(result: Result<Any?>): Any? { this.result = result this.label = this.label or BIT_REUSE_OK return requireSuspending(0L, this as Continuation<Any?>) } } val continuation = when { completion is RequireContinuation && (completion.label and BIT_REUSE_OK) != 0 -> { completion.label -= BIT_REUSE_OK completion } else -> RequireContinuation(completion) } val requestResult = when (continuation.label) { 0 -> { continuation.result.throwOnFailure() continuation.label = 1 val requestResultOrSuspended = request(byteCount, continuation) if (requestResultOrSuspended === COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED requestResultOrSuspended as Boolean } 1 -> { continuation.result.throwOnFailure() continuation.result as Boolean } else -> throw IllegalStateException("call to 'resume' before 'invoke' with coroutine") } if (!requestResult) { throw EOFException() } return Unit }
  11. fun requireSuspending( byteCount: Long, completion: Continuation<Any?>, ): Any? { class

    RequireContinuation(completion: Continuation<Any?>) : ContinuationImpl(completion) { var result: Any? = null var label: Int = 0 public override fun invokeSuspend(result: Result<Any?>): Any? { this.result = result this.label = this.label or BIT_REUSE_OK return requireSuspending(0L, this as Continuation<Any?>) } } val continuation = when { completion is RequireContinuation && (completion.label and BIT_REUSE_OK) != 0 -> { completion.label -= BIT_REUSE_OK completion } else -> RequireContinuation(completion) }
  12. fun requireSuspending( byteCount: Long, completion: Continuation<Any?>, ): Any? { class

    RequireContinuation(completion: Continuation<Any?>) : ContinuationImpl(completion) { var result: Any? = null var label: Int = 0 public override fun invokeSuspend(result: Result<Any?>): Any? { this.result = result this.label = this.label or BIT_REUSE_OK return requireSuspending(0L, this as Continuation<Any?>) } } val continuation = when { completion is RequireContinuation && (completion.label and BIT_REUSE_OK) != 0 -> { completion.label -= BIT_REUSE_OK completion } else -> RequireContinuation(completion) }
  13. fun requireSuspending( byteCount: Long, completion: Continuation<Any?>, ): Any? { class

    RequireContinuation(completion: Continuation<Any?>) : ContinuationImpl(completion) { var result: Any? = null var label: Int = 0 public override fun invokeSuspend(result: Result<Any?>): Any? { this.result = result this.label = this.label or BIT_REUSE_OK return requireSuspending(0L, this as Continuation<Any?>) } } val continuation = when { completion is RequireContinuation && (completion.label and BIT_REUSE_OK) != 0 -> { completion.label -= BIT_REUSE_OK completion } else -> RequireContinuation(completion) }
  14. var label: Int = 0 public override fun invokeSuspend(result: Result<Any?>):

    Any? { this.result = result this.label = this.label or BIT_REUSE_OK return requireSuspending(0L, this as Continuation<Any?>) } } val continuation = when { completion is RequireContinuation && (completion.label and BIT_REUSE_OK) != 0 -> { completion.label -= BIT_REUSE_OK completion } else -> RequireContinuation(completion) } val requestResult = when (continuation.label) { 0 -> { continuation.result.throwOnFailure() continuation.label = 1 val requestResultOrSuspended = request(byteCount, continuation)
  15. } else -> RequireContinuation(completion) } val requestResult = when (continuation.label)

    { 0 -> { continuation.result.throwOnFailure() continuation.label = 1 val requestResultOrSuspended = request(byteCount, continuation) if (requestResultOrSuspended === COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED requestResultOrSuspended as Boolean } 1 -> { continuation.result.throwOnFailure() continuation.result as Boolean } else -> throw IllegalStateException("call to 'resume' before 'invoke' with coroutine") } if (!requestResult) { throw EOFException()
  16. } else -> RequireContinuation(completion) } val requestResult = when (continuation.label)

    { 0 -> { continuation.result.throwOnFailure() continuation.label = 1 val requestResultOrSuspended = request(byteCount, continuation) if (requestResultOrSuspended === COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED requestResultOrSuspended as Boolean } 1 -> { continuation.result.throwOnFailure() continuation.result as Boolean } else -> throw IllegalStateException("call to 'resume' before 'invoke' with coroutine") } if (!requestResult) { throw EOFException()
  17. val requestResult = when (continuation.label) { 0 -> { continuation.result.throwOnFailure()

    continuation.label = 1 val requestResultOrSuspended = request(byteCount, continuation) if (requestResultOrSuspended === COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED requestResultOrSuspended as Boolean } 1 -> { continuation.result.throwOnFailure() continuation.result as Boolean } else -> throw IllegalStateException("call to 'resume' before 'invoke' with coroutine") } if (!requestResult) { throw EOFException() } return Unit }
  18. val requestResult = when (continuation.label) { 0 -> { continuation.result.throwOnFailure()

    continuation.label = 1 val requestResultOrSuspended = request(byteCount, continuation) if (requestResultOrSuspended === COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED requestResultOrSuspended as Boolean } 1 -> { continuation.result.throwOnFailure() continuation.result as Boolean } else -> throw IllegalStateException("call to 'resume' before 'invoke' with coroutine") } if (!requestResult) { throw EOFException() } return Unit }
  19. fun requireSuspending( byteCount: Long, completion: Continuation<Any?>, ): Any? { class

    RequireContinuation(completion: Continuation<Any?>) : ContinuationImpl(completion) { var result: Any? = null var label: Int = 0 public override fun invokeSuspend(result: Result<Any?>): Any? { this.result = result this.label = this.label or BIT_REUSE_OK return requireSuspending(0L, this as Continuation<Any?>) } } val continuation = when { completion is RequireContinuation && (completion.label and BIT_REUSE_OK) != 0 -> { completion.label -= BIT_REUSE_OK completion } else -> RequireContinuation(completion) } val requestResult = when (continuation.label) { 0 -> { continuation.result.throwOnFailure() continuation.label = 1 val requestResultOrSuspended = request(byteCount, continuation) if (requestResultOrSuspended === COROUTINE_SUSPENDED) return COROUTINE_SUSPENDED requestResultOrSuspended as Boolean } 1 -> { continuation.result.throwOnFailure() continuation.result as Boolean } else -> throw IllegalStateException("call to 'resume' before 'invoke' with coroutine") } if (!requestResult) { throw EOFException() } return Unit }
  20. Advice Suspend is fast actually Use it everywhere Low-level I/O

    code is weird and extremely performance-sensitive
  21. Older than Kotlin! 1.0 2.0 3.0 2.6 2013 2016 2025

    2019 2.6 added a compileOnly dependency on Kotlin + coroutines! Java users don’t depend on Kotlin
  22. package java.lang.reflect; public class Proxy { public static Object newProxyInstance(

    ClassLoader loader, Class<?>[] interfaces, InvocationHandler invocationHandler ) throws IllegalArgumentException; } public interface InvocationHandler { public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable; }
  23. package java.lang.reflect; public class Proxy { public static Object newProxyInstance(

    ClassLoader loader, Class<?>[] interfaces, InvocationHandler invocationHandler ) throws IllegalArgumentException; } public interface InvocationHandler { public Object invoke( Object proxy, Method method, Object[] args ) throws Throwable; }
  24. static class SuspendingInvocationHandler implements InvocationHandler { @Override public Object invoke(

    Object proxy, Method method, Object[] args ) throws Throwable { Call<?> call = new OkHttpCall<>(args, ...); Continuation<?> continuation = (Continuation<?>) args[args.length - 1]; ... return KotlinExtensions.await(call, continuation); } }
  25. static class SuspendingInvocationHandler implements InvocationHandler { @Override public Object invoke(

    Object proxy, Method method, Object[] args ) throws Throwable { Call<?> call = new OkHttpCall<>(args, ...); Continuation<?> continuation = (Continuation<?>) args[args.length - 1]; ... return KotlinExtensions.await(call, continuation); } }
  26. static class SuspendingInvocationHandler implements InvocationHandler { @Override public Object invoke(

    Object proxy, Method method, Object[] args ) throws Throwable { Call<?> call = new OkHttpCall<>(args, ...); Continuation<?> continuation = (Continuation<?>) args[args.length - 1]; ... return KotlinExtensions.await(call, continuation); } }
  27. static class SuspendingInvocationHandler implements InvocationHandler { @Override public Object invoke(

    Object proxy, Method method, Object[] args ) throws Throwable { Call<?> call = new OkHttpCall<>(args, ...); Continuation<?> continuation = (Continuation<?>) args[args.length - 1]; ... return KotlinExtensions.await(call, continuation); } }
  28. static class SuspendingInvocationHandler implements InvocationHandler { @Override public Object invoke(

    Object proxy, Method method, Object[] args ) throws Throwable { Call<?> call = new OkHttpCall<>(args, ...); Continuation<?> continuation = (Continuation<?>) args[args.length - 1]; ... return KotlinExtensions.await(call, continuation); } }
  29. Java + Coroutines Java signatures have an additional argument, a

    Continuation Receive it in a dynamic proxy Pass it when calling Kotlin from Java
  30. Gotchas Kotlin coroutines and Java don’t really interop Java wraps

    exceptions with UndeclaredThrowableException Work around that by forcing a suspend!
  31. interface HttpCall { fun cancel() fun execute(): Response fun enqueue(callback:

    HttpCallback) } interface HttpCallback { fun onResponse(response: Response) fun onFailure(e: IOException) } suspend fun executeAsync(httpCall: HttpCall): Response { // how? }
  32. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCoroutine { continuation

    -> httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } 1 ATTEMPT
  33. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCoroutine { continuation

    -> httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } 1 ATTEMPT
  34. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCoroutine { continuation

    -> httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } 1 ATTEMPT
  35. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCoroutine { continuation

    -> httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } 1 ATTEMPT
  36. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCoroutine { continuation

    -> httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } 1 ATTEMPT
  37. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCoroutine { continuation

    -> httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } 1 ATTEMPT
  38. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCoroutine { continuation

    -> httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } Canceling the coroutine does nothing FAIL 1 ATTEMPT
  39. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCoroutine { continuation

    -> httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } 1 ATTEMPT
  40. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCancellableCoroutine { continuation

    -> httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } 2 ATTEMPT
  41. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCancellableCoroutine { continuation

    -> httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } 2 ATTEMPT
  42. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCancellableCoroutine { continuation

    -> httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } Canceling the coroutine doesn’t cancel the HTTP call FAIL 2 ATTEMPT
  43. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCancellableCoroutine { continuation

    -> continuation.invokeOnCancellation { httpCall.cancel() } httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } 3 ATTEMPT
  44. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCancellableCoroutine { continuation

    -> continuation.invokeOnCancellation { httpCall.cancel() } httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } 3 ATTEMPT
  45. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCancellableCoroutine { continuation

    -> continuation.invokeOnCancellation { httpCall.cancel() } httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } The response isn’t closed if it’s canceled after onResponse FAIL 3 ATTEMPT
  46. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCancellableCoroutine { continuation

    -> continuation.invokeOnCancellation { httpCall.cancel() } httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) { cause, value, context -> response.close() } } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } 4 ATTEMPT
  47. suspend fun executeAsync(httpCall: HttpCall): Response { return suspendCancellableCoroutine { continuation

    -> continuation.invokeOnCancellation { httpCall.cancel() } httpCall.enqueue( object : HttpCallback { override fun onResponse(response: Response) { continuation.resume(response) { cause, value, context -> response.close() } } override fun onFailure(e: IOException) { continuation.resumeWithException(e) } } ) } } 4 ATTEMPT Cancel works & nothing leaks!
  48. Let’s do some math 3 = 3 3+3 = 6

    3+3+3+3+3+3+3+3+3+3+3+3+3+3+3+3+3+3+3+3+3+3+3+3+3 = 72
  49. fun Buffer.evaluate(): Long { val value = when (val peek

    = this[0].toInt().toChar()) { in '0'..'9' -> readDecimalLong() else -> error("unexpected expression $peek") } if (exhausted()) return value return when (val operator = readByte().toInt().toChar()) { '+' -> value + evaluate() '*' -> value * evaluate() else -> error("unexpected expression $operator") } } Let’s write some code
  50. fun Buffer.evaluate(): Long { val value = when (val peek

    = this[0].toInt().toChar()) { in '0'..'9' -> readDecimalLong() else -> error("unexpected expression $peek") } if (exhausted()) return value return when (val operator = readByte().toInt().toChar()) { '+' -> value + evaluate() '*' -> value * evaluate() else -> error("unexpected expression $operator") } } Let’s write some code
  51. fun Buffer.evaluate(): Long { val value = when (val peek

    = this[0].toInt().toChar()) { in '0'..'9' -> readDecimalLong() else -> error("unexpected expression $peek") } if (exhausted()) return value return when (val operator = readByte().toInt().toChar()) { '+' -> value + evaluate() '*' -> value * evaluate() else -> error("unexpected expression $operator") } } Let’s write some code
  52. fun Buffer.evaluate(): Long { val value = when (val peek

    = this[0].toInt().toChar()) { in '0'..'9' -> readDecimalLong() else -> error("unexpected expression $peek") } if (exhausted()) return value return when (val operator = readByte().toInt().toChar()) { '+' -> value + evaluate() '*' -> value * evaluate() else -> error("unexpected expression $operator") } } Let’s write some code
  53. fun Buffer.evaluate(): Long { val value = when (val peek

    = this[0].toInt().toChar()) { in '0'..'9' -> readDecimalLong() else -> error("unexpected expression $peek") } if (exhausted()) return value return when (val operator = readByte().toInt().toChar()) { '+' -> value + evaluate() '*' -> value * evaluate() else -> error("unexpected expression $operator") } } Let’s write some code
  54. class EvaluateTest { @Test fun happyPath() { assertThat(evaluate("3")).isEqualTo(3) assertThat(evaluate("3+3+3")).isEqualTo(9) }

    @Test fun veryRecursive() { assertThat(evaluate("1+".repeat(1234) + "0")).isEqualTo(1234) assertThat(evaluate("1+".repeat(12345) + "0")).isEqualTo(12345) } } Let’s test
  55. class EvaluateTest { @Test fun happyPath() { assertThat(evaluate("3")).isEqualTo(3) assertThat(evaluate("3+3+3")).isEqualTo(9) }

    @Test fun veryRecursive() { assertThat(evaluate("1+".repeat(1234) + "0")).isEqualTo(1234) assertThat(evaluate("1+".repeat(12345) + "0")).isEqualTo(12345) } } Let’s test
  56. class EvaluateTest { @Test fun happyPath() { assertThat(evaluate("3")).isEqualTo(3) assertThat(evaluate("3+3+3")).isEqualTo(9) }

    @Test fun veryRecursive() { assertThat(evaluate("1+".repeat(1234) + "0")).isEqualTo(1234) assertThat(evaluate("1+".repeat(12345) + "0")).isEqualTo(12345) } } Let’s test
  57. fun Buffer.evaluate(): Long { val value = when (val peek

    = this[0].toInt().toChar()) { in '0'..'9' -> readDecimalLong() else -> error("unexpected expression $peek") } if (exhausted()) return value return when (val operator = readByte().toInt().toChar()) { '+' -> value + evaluate() '*' -> value * evaluate() else -> error("unexpected expression $operator") } } Recursion!
  58. fun Buffer.evaluate(): Long { val value = when (val peek

    = this[0].toInt().toChar()) { in '0'..'9' -> readDecimalLong() else -> error("unexpected expression $peek") } if (exhausted()) return value return when (val operator = readByte().toInt().toChar()) { '+' -> value + evaluate() '*' -> value * evaluate() else -> error("unexpected expression $operator") } } Recursion! Recursion!
  59. fun Buffer.evaluate(): Long { val value = when (val peek

    = this[0].toInt().toChar()) { in '0'..'9' -> readDecimalLong() else -> error("unexpected expression $peek") } if (exhausted()) return value return when (val operator = readByte().toInt().toChar()) { '+' -> value + evaluate() '*' -> value * evaluate() else -> error("unexpected expression $operator") } } Recursion! Recursion! Recursion!
  60. fun Buffer.evaluate(): Long { val value = when (val peek

    = this[0].toInt().toChar()) { in '0'..'9' -> readDecimalLong() else -> error("unexpected expression $peek") } if (exhausted()) return value return when (val operator = readByte().toInt().toChar()) { '+' -> value + evaluate() '*' -> value * evaluate() else -> error("unexpected expression $operator") } } Let’s coroutines?
  61. fun suspend Buffer.evaluate(): Long { val value = when (val

    peek = this[0].toInt().toChar()) { in '0'..'9' -> readDecimalLong() else -> error("unexpected expression $peek") } if (exhausted()) return value return when (val operator = readByte().toInt().toChar()) { '+' -> value + evaluate() '*' -> value * evaluate() else -> error("unexpected expression $operator") } } Let’s coroutines?
  62. suspend fun Buffer.evaluate(): Long { val value = when (val

    peek = this[0].toInt().toChar()) { in '0'..'9' -> readDecimalLong() else -> error("unexpected expression $peek") } if (exhausted()) return value return when (val operator = readByte().toInt().toChar()) { '+'Z->ZvalueZ+Zevaluate() '*'T->TvalueT*Tevaluate() else -> error("unexpected expression $operator") } } Let’s coroutines... harder?
  63. suspend fun Buffer.evaluate(): Long { val value = when (val

    peek = this[0].toInt().toChar()) { in '0'..'9' -> readDecimalLong() else -> error("unexpected expression $peek") } if (exhausted()) return value yield() return when (val operator = readByte().toInt().toChar()) { '+'Z->ZvalueZ+Zevaluate() '*'T->TvalueT*Tevaluate() else -> error("unexpected expression $operator") } } Let’s coroutines... harder?
  64. Why Does It Work? Calling a function pushes a frame

    on the stack • This applies to regular functions and suspend functions! • Calling 12,345 functions causes a StackOverflowError But yield() clears the stack! (Each stack frame is in memory, just not on the stack)
  65. Quadratic Execution Time? IntelliJ synthesizes stack traces for coroutines •

    Only in the debugger • Only when it’s launched from IntelliJ This is a great feature Absolutely not a performance problem in practice
  66. Advice Don’t use yield() to get smaller stack frames You

    will go to programmer jail Check out DeepRecursiveFunction if you need that
  67. Let’s Scan the FileSystem Goals: • Recursive - return all

    the paths in a subtree, ‘depth-first’ • Streaming - the file system could be huge! We should return some results before we’ve visited the whole thing • Simple - both as a user and as implementor
  68. class FileTreeWalker implements Closeable { private final boolean followLinks; private

    final LinkOption[] linkOptions; private final int maxDepth; private final ArrayDeque<DirectoryNode> stack = new ArrayDeque<>(); private boolean closed; /** * The element on the walking stack corresponding to a directory node. */ private static class DirectoryNode { private final Path dir; private final Object key; private final DirectoryStream<Path> stream; private final Iterator<Path> iterator; private boolean skipped; Java’s Implementation
  69. } /** * Returns {@code true} if the walker is

    open. */ boolean isOpen() { return !closed; } /** * Closes/pops all directories on the stack. */ @Override public void close() { if (!closed) { while (!stack.isEmpty()) { pop(); } closed = true; } } } Java’s Implementation
  70. Java’s is complex Lots of features: • Symlinks • Streaming

    contents of directories But also lots of computer science: • Needs to keep state for the position in each parent directory • Uses an ArrayDeque<DirectoryNode>
  71. interface FileSystem { fun listOrNull(dir: Path): List<Path>? // ... fun

    listRecursively(dir: Path): Sequence<Path> { return sequence { for (child in listOrNull(dir)!!) { collectRecursively(child) } } } private suspend fun SequenceScope<Path>.collectRecursively(path: Path) { Sequence Implementation
  72. fun listOrNull(dir: Path): List<Path>? // ... fun listRecursively(dir: Path): Sequence<Path>

    { return sequence { for (child in listOrNull(dir)!!) { collectRecursively(child) } } } private suspend fun SequenceScope<Path>.collectRecursively(path: Path) { yield(path) val children = listOrNull(path) ?: return for (child in children) { collectRecursively(child) } } } Sequence Implementation
  73. fun listOrNull(dir: Path): List<Path>? // ... fun listRecursively(dir: Path): Sequence<Path>

    { return sequence { for (child in listOrNull(dir)!!) { collectRecursively(child) } } } private suspend fun SequenceScope<Path>.collectRecursively(path: Path) { yield(path) val children = listOrNull(path) ?: return for (child in children) { collectRecursively(child) } } } Sequence Implementation
  74. fun listOrNull(dir: Path): List<Path>? // ... fun listRecursively(dir: Path): Sequence<Path>

    { return sequence { for (child in listOrNull(dir)!!) { collectRecursively(child) } } } private suspend fun SequenceScope<Path>.collectRecursively(path: Path) { yield(path) val children = listOrNull(path) ?: return for (child in children) { collectRecursively(child) } } } Sequence Implementation
  75. fun listOrNull(dir: Path): List<Path>? // ... fun listRecursively(dir: Path): Sequence<Path>

    { return sequence { for (child in listOrNull(dir)!!) { collectRecursively(child) } } } private suspend fun SequenceScope<Path>.collectRecursively(path: Path) { yield(path) val children = listOrNull(path) ?: return for (child in children) { collectRecursively(child) } } } Sequence Implementation
  76. fun listOrNull(dir: Path): List<Path>? // ... fun listRecursively(dir: Path): Sequence<Path>

    { return sequence { for (child in listOrNull(dir)!!) { collectRecursively(child) } } } private suspend fun SequenceScope<Path>.collectRecursively(path: Path) { yield(path) val children = listOrNull(path) ?: return for (child in children) { collectRecursively(child) } } } Sequence Implementation
  77. fun listOrNull(dir: Path): List<Path>? // ... fun listRecursively(dir: Path): Sequence<Path>

    { return sequence { for (child in listOrNull(dir)!!) { collectRecursively(child) } } } private suspend fun SequenceScope<Path>.collectRecursively(path: Path) { yield(path) val children = listOrNull(path) ?: return for (child in children) { collectRecursively(child) } } } Sequence Implementation
  78. That’s it?! We use the call stack to track where

    we are in the traversal Kotlin’s sequence {} API manages that call stack between yielded values
  79. It’s Restricted! @RestrictsSuspension @SinceKotlin("1.3") public abstract class SequenceScope<in T> {

    public abstract suspend fun yield(value: T) public abstract suspend fun yieldAll(iterator: Iterator<T>) public suspend fun yieldAll(elements: Iterable<T>) public suspend fun yieldAll(sequence: Sequence<T>) }
  80. Easy put & get API Scales from a couple elements

    to millions val map = HashMap<String, String>() map["M"] = "Mushrooms" map["H"] = "Ham" println(map["M"]) I Love Hash Maps
  81. Hash Table Refresher! Kotlin’s mapOf() and mutableMapOf() functions return implementations

    of the hash table data structure It’s general-purpose: it’ll efficiently store 5 elements or 5,000,000 Kotlin’s implementations are different on different platforms
  82. The Computer Science Every value has a hashCode() function that

    returns an Int • If two values are equal, they must return the same Int • If two values are different, they should return different Ints Use the Int to pick an index in the hash array Lookups are fast ’cause we’ll often find our item immediately
  83. 1_ HashMap table size 0 modCount 0 threshold 12 loadFactor

    0.75 Array<Entry> size 16 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 String value "M" hash 0 String value "Mushrooms" hash 0 Entry hash 77 key value next 77 val map Elements
  84. 2_ Entry hash 72 key value next HashMap table size

    0 modCount 0 threshold 12 loadFactor 0.75 Array<Entry> size 16 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 String value "H" hash 0 String value "Ham" hash 0 String value "M" hash 0 String value "Mushrooms" hash 0 Entry hash 77 key value next 1 1 2 2 77 72 val map Elements
  85. Memory Overhead The HashMap object The Array<Entry> that’s between 33%

    and 167% larger than N N Entry objects Classes like String have extra fields to cache hash codes
  86. Compute Overhead Each get() accesses 4 objects (map, entries array,

    entry, key) Computing the hash code needs to traverse the entire key object • There’s a reason this is cached! Checking equals() needs to traverse two objects Extra garbage collection!
  87. Hash Tables are Great They are fast actually Use them

    everywhere Coroutines’ implementation details are weird and extremely performance-sensitive
  88. interface CoroutineContext { operator fun plus(context: CoroutineContext): CoroutineContext operator fun

    <E : Element> get(key: Key<E>): E? } CoroutineContext is Map-like Uses plus() instead of put() Returns a new instance rather than mutating
  89. fun doMapStuff() { var map: CoroutineContext = EmptyCoroutineContext map +=

    Topping(Topping.Key("M"), "Mushrooms") map += Topping(Topping.Key("H"), "Ham") println(map[Topping.Key("M")]) } data class Topping( override val key: Key, val name: String ) : CoroutineContext.Element { data class Key(val letter: String): CoroutineContext.Key<Topping> } Really Map-Like
  90. 0 1 1 + N 3 Kinds EmptyCoroutineContext CoroutineContext.Element Includes

    CoroutineDispatcher, CoroutineName, CoroutineExceptionHandler, Job, etc. CombinedContext
  91. Elements 1_ String value "M" hash 0 Topping key value

    Topping.Key letter String value "Mushrooms" hash 0 var map Elements
  92. Elements 2_ String value "M" hash 0 Topping key value

    Topping.Key letter String value "H" hash 0 String value "Ham" hash 0 String value "Mushrooms" hash 0 var map CombinedContext left element Topping key value Topping.Key letter Elements
  93. var map String value "P" hash 0 String value "Pineapple"

    hash 0 CombinedContext left element Topping key value Topping.Key letter key value letter CombinedContext left element Topping key value Topping.Key letter Elements 3_
  94. It’s a Map! It’s a Linked List! Looking up a

    key in CoroutineContext takes N steps Thankfully, N is very small in practice
  95. Three more optimizations! Keys are singleton constants The entry and

    the value is the same object The CoroutineDispatcher is always the head of the linked list
  96. String value "M" hash 0 Topping key value Topping.Key letter

    String value "H" hash 0 String value "Ham" hash 0 String value "Mushrooms" hash 0 CombinedContext left element Topping.Key letter Topping key value
  97. CoroutineDispatcher key value CoroutineDispatcher.Key String value "Ham" hash 0 String

    value "Mushrooms" hash 0 CombinedContext left element Job.Key Job key value Keys are singleton constants
  98. CombinedContext left element CoroutineDispatcher key queue nextTime isCompleted delayed CoroutineDispatcher.Key

    Job.Key Job key parent state The entry and the value is the same object
  99. CombinedContext left element CoroutineDispatcher key queue nextTime isCompleted delayed CoroutineDispatcher.Key

    Job.Key Job key parent state The CoroutineDispatcher is always the head of the linked list
  100. Try/Catch is Rad We expect some operations to sometimes fail

    Use try/catch for safe & easy error handling
  101. Exception Handlers are Also Rad Operations also fail in ways

    we don’t expect Add a CoroutineExceptionHandler to your CoroutineContext It’ll get called if the coroutine throws an exception that nobody catches
  102. 100% Conformance is Too Hard We might not register our

    CoroutineExceptionHandler everywhere • Perhaps we forgot! • Perhaps it’s 3rd party code
  103. How Java does it Java’s Thread has two properties: •

    uncaughtExceptionHandler • defaultUncaughtExceptionHandler, as a fallback
  104. Unless... /** * Installs a global coroutine exception handler using

    an internal API. */ @Suppress("INVISIBLE_MEMBER", "INVISIBLE_REFERENCE") internal fun registerUncaughtExceptionHandler( coroutineExceptionHandler: CoroutineExceptionHandler, ) { kotlinx.coroutines.internal.ensurePlatformExceptionHandlerLoaded( object : CoroutineExceptionHandler by coroutineExceptionHandler { override fun handleException(context: CoroutineContext, exception: Throwable) { coroutineExceptionHandler.handleException(context, exception) throw kotlinx.coroutines.internal.ExceptionSuccessfullyProcessed } }, ) }
  105. Advice Using internal APIs feels bad • It makes it

    dangerous to update libraries! • Vote this up: github.com/Kotlin/kotlinx.coroutines/issues/3978 But hard crashes feel very bad
  106. Your Server is a Function We pretend a remote API

    is a function we can call! • Take a request object • Encode it as bytes & transmit it • Await response • Decode it and return it Not a perfect abstraction!
  107. Your User is a Function We pretend a person is

    a function we can call! • Take a decision • Encode it as pixels & display it • Await clicks, taps, and keystrokes • Model it and return it Not a perfect abstraction!
  108. Here’s a Pizza REST API POST http://localhost:80/api/order-pizza Content-Type: application/json Accept:

    application/json { "address": {"line_1": "Building 293, Brooklyn NY"}, "pizzas": [{"size": "medium", "toppings": ["pineapple", "onions"]}] } HTTP/1.1 200 OK Content-Type: application/json { "id": "CPT1001", "status": "pending", "eta": "2025-05026T14:03:00Z" }
  109. Here’s a Pizza REST API POST http://localhost:80/api/order-pizza Content-Type: application/json Accept:

    application/json { "address": {"line_1": "Building 293, Brooklyn NY"}, "pizzas": [{"size": "medium", "toppings": ["pineapple", "onions"]}] } HTTP/1.1 200 OK Content-Type: application/json { "id": "CPT1001", "status": "pending", "eta": "2025-05026T14:03:00Z" } PARAMETER
  110. Here’s a Pizza REST API POST http://localhost:80/api/order-pizza Content-Type: application/json Accept:

    application/json { "address": {"line_1": "Building 293, Brooklyn NY"}, "pizzas": [{"size": "medium", "toppings": ["pineapple", "onions"]}] } HTTP/1.1 200 OK Content-Type: application/json { "id": "CPT1001", "status": "pending", "eta": "2025-05026T14:03:00Z" } RETURN VALUE PARAMETER
  111. Call the server like a function val orderResponse = pizzaApi.order(

    OrderRequest( Address("Building 293, Brooklyn NY"), listOf(pizza), ) ) The order() function suspends while the server does its thing It returns the server’s decision
  112. class GetPizzaScreen(spec: GetPizzaScreenSpec) { @Composable override fun Show(onResult: (Pizza) ->

    Unit) { FlowScreen( body = { SizeSection() KindSection() }, footer = { button( text = "Order ${price().formattedWithCents()}", onClick = { onResult(Pizza(size.value, kind.value.toppings)) }, Here’s a Pizza UI
  113. class GetPizzaScreen(spec: GetPizzaScreenSpec) { @Composable override fun Show(onResult: (Pizza) ->

    Unit) { FlowScreen( body = { SizeSection() KindSection() }, footer = { button( text = "Order ${price().formattedWithCents()}", onClick = { onResult(Pizza(size.value, kind.value.toppings)) }, Here’s a Pizza UI PARAMETER
  114. class GetPizzaScreen(spec: GetPizzaScreenSpec) { @Composable override fun Show(onResult: (Pizza) ->

    Unit) { FlowScreen( body = { SizeSection() KindSection() }, footer = { button( text = "Order ${price().formattedWithCents()}", onClick = { onResult(Pizza(size.value, kind.value.toppings)) }, Here’s a Pizza UI RETURN VALUE! PARAMETER
  115. Call the user like a function! The display.show() call suspends

    while the user does her thing It returns her decision val pizza = display.show(GetPizzaScreenSpec())
  116. WHY? Abstracting a server into a function call makes sense,

    it’s software Abstracting a person into a function is a cool party trick, but why?! val pizza = display.show(GetPizzaScreenSpec()) WHY? WHY?
  117. It’s not just one screen! override suspend fun execute(display: Display)

    { val pizza = display.show(GetPizzaScreenSpec()) display.show(ConfirmScreenSpec(pizza)) val orderResponse = pizzaApi.order( OrderRequest(pizza) ) display.show( ResultScreenSpec( eta = orderResponse.eta, ) ) }
  118. It’s not just one screen! override suspend fun execute(display: Display)

    { val pizza = display.show(GetPizzaScreenSpec()) display.show(ConfirmScreenSpec(pizza)) val orderResponse = pizzaApi.order( OrderRequest(pizza) ) display.show( ResultScreenSpec( eta = orderResponse.eta, ) ) }
  119. Let’s Change it Up Let’s upsell: • The confirm screen

    used to return Unit • Now it’ll return either or AddAnother or Proceed And our navigation function needs a loop
  120. override suspend fun execute(display: Display) { var pizzas = listOf<Pizza>()

    while (true) { pizzas += display.show(GetPizzaScreenSpec()) when (display.show(ConfirmScreenSpec(pizzas))) { AddAnother -> continue Proceed -> break } } val orderResponse = pizzaApi.order( OrderRequest(pizzas) ) display.show( ResultScreenSpec( eta = orderResponse.eta ) ) }
  121. override suspend fun execute(display: Display) { var pizzas = listOf<Pizza>()

    while (true) { pizzas += display.show(GetPizzaScreenSpec()) when (display.show(ConfirmScreenSpec(pizzas))) { AddAnother -> continue Proceed -> break } } val orderResponse = pizzaApi.order( OrderRequest(pizzas) ) display.show( ResultScreenSpec( eta = orderResponse.eta ) ) }
  122. Navigation is configured as a set of edges in a

    graph Navigation is separate from application data Extremely flexible for deep links & synthetic back stacks Awkward to test! NavController Navigation is what happens when you call a function to show a screen Navigation is integrated with application data Strictly structured - No GOTO Easy to test Functions
  123. Navigation is configured as a set of edges in a

    graph Navigation is separate from application data Extremely flexible for deep links & synthetic back stacks Awkward to test! NavController Navigation is what happens when you call a function to show a screen Navigation is integrated with application data Strictly structured - No GOTO Easy to test Functions
  124. Navigation is configured as a set of edges in a

    graph Navigation is separate from application data Extremely flexible for deep links & synthetic back stacks Awkward to test! NavController Navigation is what happens when you call a function to show a screen Navigation is integrated with application data Strictly structured - No GOTO Easy to test Functions
  125. Navigation is configured as a set of edges in a

    graph Navigation is separate from application data Extremely flexible for deep links & synthetic back stacks Awkward to test! NavController Navigation is what happens when you call a function to show a screen Navigation is integrated with application data Strictly structured - No GOTO Easy to test Functions
  126. Navigation is configured as a set of edges in a

    graph Navigation is separate from application data Extremely flexible for deep links & synthetic back stacks Awkward to test! NavController Navigation is what happens when you call a function to show a screen Navigation is integrated with application data Strictly structured - No GOTO Easy to test Functions
  127. A Structured Navigation API interface ScreenSpec<T> interface Screen<T> { @Composable

    fun Show(onResult: (T) -> Unit) interface Factory { fun <T> create( spec: ScreenSpec<T>, ): Screen<T>? } } interface App { suspend fun execute(display: Display) } interface Display { suspend fun <T> show( spec: ScreenSpec<T> ): T } @Composable fun Show( app: App, screenFactory: Screen.Factory, ) { ... } object LoadingScreenSpec : ScreenSpec<Nothing>
  128. Back Button Design idea: the call stack is the back

    stack Add a new show() overload that accepts a lambda Pressing back while executing that lambda will return to the screen interface Display { suspend fun <T> show(spec: ScreenSpec<T>): T suspend fun <T, R> show(spec: ScreenSpec<T>, block: suspend (T) -> R): R }
  129. override suspend fun execute(display: Display) { val pizza = display.show(GetPizzaScreenSpec())

    { pizza -> display.show(ConfirmScreenSpec(pizza)) return@show pizza } ... }
  130. How it works 1. Run the lambda as a job

    using async() 2. If the user presses back, cancel that job 3. Recover from canceled jobs by showing the previous screen
  131. Goal We want a push transition between the current and

    next screen Unless there’s a loading delay, in which case we’ll fade to a spinner We don’t know ahead of time if there will be a loading delay
  132. Coroutine Dispatchers When a coroutine suspends, it builds a Runnable

    to run when it’s resumed later Upon resume, it needs something to execute that Runnable That something is a CoroutineDispatcher
  133. Strategy Decorate CoroutineDispatcher Every time a block is enqueued, increment

    enqueueCount Every time a block completes, increment completeCount Any time enqueueCount equals completeCount, we’re idle!
  134. class IdleCallbackCoroutineDispatcher( private val delegate: CoroutineDispatcher, private val onIdle: ()

    -> Unit, ) : CoroutineDispatcher() { private var enqueueCount = 0 private var completeCount = 0 override fun dispatch(context: CoroutineContext, block: Runnable) { enqueueCount++ delegate.dispatch(context) { try { block.run() } finally { completeCount++ if (enqueueCount == completeCount) { onIdle() } } } } }
  135. class IdleCallbackCoroutineDispatcher( private val delegate: CoroutineDispatcher, private val onIdle: ()

    -> Unit, ) : CoroutineDispatcher() { private var enqueueCount = 0 private var completeCount = 0 override fun dispatch(context: CoroutineContext, block: Runnable) { enqueueCount++ delegate.dispatch(context) { try { block.run() } finally { completeCount++ if (enqueueCount == completeCount) { onIdle() } } } } }
  136. class IdleCallbackCoroutineDispatcher( private val delegate: CoroutineDispatcher, private val onIdle: ()

    -> Unit, ) : CoroutineDispatcher() { private var enqueueCount = 0 private var completeCount = 0 override fun dispatch(context: CoroutineContext, block: Runnable) { enqueueCount++ delegate.dispatch(context) { try { block.run() } finally { completeCount++ if (enqueueCount == completeCount) { onIdle() } } } } }
  137. class IdleCallbackCoroutineDispatcher( private val delegate: CoroutineDispatcher, private val onIdle: ()

    -> Unit, ) : CoroutineDispatcher() { private var enqueueCount = 0 private var completeCount = 0 override fun dispatch(context: CoroutineContext, block: Runnable) { enqueueCount++ delegate.dispatch(context) { try { block.run() } finally { completeCount++ if (enqueueCount == completeCount) { onIdle() } } } } }
  138. class IdleCallbackCoroutineDispatcher( private val delegate: CoroutineDispatcher, private val onIdle: ()

    -> Unit, ) : CoroutineDispatcher() { private var enqueueCount = 0 private var completeCount = 0 override fun dispatch(context: CoroutineContext, block: Runnable) { enqueueCount++ delegate.dispatch(context) { try { block.run() } finally { completeCount++ if (enqueueCount == completeCount) { onIdle() } } } } }
  139. class IdleCallbackCoroutineDispatcher( private val delegate: CoroutineDispatcher, private val onIdle: ()

    -> Unit, ) : CoroutineDispatcher() { private var enqueueCount = 0 private var completeCount = 0 override fun dispatch(context: CoroutineContext, block: Runnable) { enqueueCount++ delegate.dispatch(context) { try { block.run() } finally { completeCount++ if (enqueueCount == completeCount) { onIdle() } } } } }
  140. class IdleCallbackCoroutineDispatcher( private val delegate: CoroutineDispatcher, private val onIdle: ()

    -> Unit, ) : CoroutineDispatcher() { private var enqueueCount = 0 private var completeCount = 0 override fun dispatch(context: CoroutineContext, block: Runnable) { enqueueCount++ delegate.dispatch(context) { try { block.run() } finally { completeCount++ if (enqueueCount == completeCount) { onIdle() } } } } }
  141. class IdleCallbackCoroutineDispatcher( private val delegate: CoroutineDispatcher, private val onIdle: ()

    -> Unit, ) : CoroutineDispatcher() { private var enqueueCount = 0 private var completeCount = 0 override fun dispatch(context: CoroutineContext, block: Runnable) { enqueueCount++ delegate.dispatch(context) { try { block.run() } finally { completeCount++ if (enqueueCount == completeCount) { onIdle() } } } } }
  142. val originalDispatcher = scope.coroutineContext[CoroutineDispatcher] ?: error("no coroutine dispatcher?") val onIdleDispatcher

    = originalDispatcher.withOnIdleCallback { maybeShowLoading() } scope.launch(onIdleDispatcher) { app.execute(display) } Using It
  143. Gotcha: Infinite Idle The idle callback might dispatch a task

    When that task completes, we’re idle again Oh no! Infinite loop
  144. Gotcha: Delay The built-in implementations of CoroutineDispatcher implement an internal

    API, kotlin.coroutines.Delay When we decorate, we lose the interface This breaks tests!
  145. Advice The decorator pattern is powerful Decorating core framework types

    can be fragile Watch out for internal-facing APIs like Delay
  146. Let’s Party Coroutines do a lot for you They’re fast

    but not free Don’t go to programmer jail