Kotlin Advanced: Coroutines, DSLs & Multiplatform
Kotlin’s coroutines and multiplatform capabilities set it apart from other JVM languages. Coroutines provide structured concurrency with no threading overhead, while Kotlin Multiplatform shares business logic across Android, iOS, and web.
In this tutorial, you’ll master coroutine internals, Flow and Channel, type-safe DSL builders, Kotlin Multiplatform project setup, kotlinx.serialization, and an introduction to compiler plugins.
What You’ll Learn
- Coroutine internals: Continuation, suspend, CoroutineContext
- Structured concurrency: scope, cancellation, exception handling
- Flow: cold streams, operators, backpressure
- Channel: hot streams, rendezvous, buffered, conflated
- DSL builders: type-safe receivers, invocations, scope control
- Kotlin Multiplatform (KMP) project structure
- kotlinx.serialization for multiplatform serialization
Why Advanced Kotlin Matters
Kotlin powers Android development, server-side applications with Ktor and Spring Boot, and cross-platform mobile apps with KMP. At DodaTech, Doda Browser’s Android component uses coroutines for async networking and Flow for reactive UI state.
Learning Path
flowchart LR
A[Kotlin Basics] --> B[Kotlin Advanced<br/>You are here]
B --> C[Coroutines Deep Dive]
B --> D[KMP Projects]
style B fill:#f90,color:#fff
Coroutine Internals
Behind the scenes, suspend functions compile to state machines using the Continuation interface:
import kotlinx.coroutines.*
// Suspend function with continuation
suspend fun fetchData(id: Int): String {
return withContext(Dispatchers.IO) {
delay(100) // Simulate network
"Data for $id"
}
}
// CoroutineScope with structured concurrency
fun main() = runBlocking {
// CoroutineScope provides structured concurrency
// When scope is cancelled, all children are cancelled
val result = coroutineScope {
val deferred = async {
fetchData(42)
}
deferred.await()
}
println(result) // Data for 42
}
// Custom context element
class MyContext(val name: String) : AbstractCoroutineContextElement(Key) {
companion object Key : CoroutineContext.Key<MyContext>
}
fun CoroutineScope.myContext(): MyContext =
coroutineContext[MyContext] ?: MyContext("default")Expected behavior: async creates a child coroutine that runs concurrently. coroutineScope waits for all children. If any child fails, the scope cancels all children.
Structured Concurrency
fun main() = runBlocking {
// Parent scope
supervisorScope {
// Child 1 — fails
launch {
delay(100)
throw RuntimeException("Child 1 failed")
}
// Child 2 — continues despite child 1 failure
launch {
try {
delay(500)
println("Child 2 completed")
} catch (e: CancellationException) {
println("Child 2 cancelled")
}
}
delay(1000)
}
// Child 2 completes because supervisorScope doesn't
// propagate failures between children
}Expected behavior: With supervisorScope, child 2 continues running even after child 1 fails. With coroutineScope, child 1’s failure cancels child 2 immediately.
Flow — Cold Asynchronous Streams
Flow is a cold stream — no code runs until collected:
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*
// Create a flow
fun numberFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(100)
emit(i) // Emit value
}
}
fun main() = runBlocking {
// Terminal operators
numberFlow()
.map { it * 2 }
.filter { it > 5 }
.catch { e -> println("Error: $e") }
.collect { println(it) }
// 6, 8, 10 (only values > 5 after doubling)
// Backpressure handling
flow {
emit(1)
emit(2)
emit(3)
}.buffer(10) // Buffer up to 10 emissions
.conflate() // Keep only the latest value
.collectLatest { value ->
// Cancel previous collection if new value arrives
delay(100)
println(value)
}
// StateFlow — state holder
val state = MutableStateFlow(0)
state.value = 1
state.collect { println(it) } // 0, 1
}Expected output: The flow emits 1-5, the map doubles them (2,4,6,8,10), filter keeps >5 (6,8,10), collect prints them. Flow operators are lazy — nothing executes until collect is called.
Channel — Hot Streams
Channels are hot — values are produced regardless of consumers:
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
fun main() = runBlocking {
// Rendezvous channel (default) — sender waits for receiver
val rendezvous = Channel<String>()
launch {
rendezvous.send("Hello")
rendezvous.send("World")
}
repeat(2) {
println(rendezvous.receive())
}
// Buffered channel — sender doesn't wait until buffer full
val buffered = Channel<String>(capacity = 10)
// Conflated channel — keeps only the latest value
val conflated = Channel<String>(CONFLATED)
// Channel as flow
val channel = Channel<Int>(Channel.UNLIMITED)
launch {
for (i in 1..5) channel.send(i)
channel.close()
}
channel.consumeAsFlow().collect { println(it) }
}Expected behavior: Rendezvous channels synchronize sender and receiver. Buffered channels decouple them. Conflated channels drop intermediate values.
DSL Builders
Type-safe DSLs are Kotlin’s superpower for building declarative APIs:
// HTML DSL example
class HTML {
private val children = mutableListOf<Element>()
fun head(init: Head.() -> Unit) {
val head = Head()
head.init()
children.add(head)
}
fun body(init: Body.() -> Unit) {
val body = Body()
body.init()
children.add(body)
}
override fun toString() = children.joinToString("")
}
class Head {
private val children = mutableListOf<String>()
fun title(text: String) {
children.add("<title>$text</title>")
}
override fun toString() = "<head>${children.joinToString("")}</head>"
}
class Body {
private val children = mutableListOf<String>()
fun h1(text: String) {
children.add("<h1>$text</h1>")
}
fun p(text: String) {
children.add("<p>$text</p>")
}
override fun toString() = "<body>${children.joinToString("")}</body>"
}
fun html(init: HTML.() -> Unit): HTML {
val html = HTML()
html.init()
return html
}
// Usage
fun main() {
val page = html {
head {
title("My Page")
}
body {
h1("Welcome")
p("This is a DSL example")
}
}
println(page)
// <head><title>My Page</title></head>
// <body><h1>Welcome</h1><p>This is a DSL example</p></body>
}Expected output: The DSL generates HTML structure. Each builder function uses init: Type.() -> Unit to create a receiver scope where methods are called without prefix.
Kotlin Multiplatform (KMP)
Share business logic across platforms:
// commonMain — shared code
expect fun platformName(): String
fun createGreeting(): String {
return "Hello from ${platformName()}!"
}
// androidMain
actual fun platformName(): String = "Android"
// iosMain
actual fun platformName(): String = "iOS"// Ktor client in commonMain
val client = HttpClient {
install(ContentNegotiation) {
json()
}
}
suspend fun fetchUsers(): List<User> {
return client.get("https://api.example.com/users").body()
}Common Mistakes
1. Not Canceling Coroutines
A coroutine that doesn’t check isActive or ensureActive() won’t respond to cancellation. Always check cancellation in long-running loops.
2. Using GlobalScope in Production
GlobalScope creates top-level coroutines that outlive the calling context. Use viewModelScope, lifecycleScope, or custom scopes instead.
3. Blocking in a Coroutine
Calling Thread.sleep() inside a coroutine blocks the thread. Use delay() instead — it suspends without blocking.
4. Collecting Flow Without a Terminal Operator
Flow operators like map, filter are intermediate. Nothing happens until a terminal operator (collect, toList, first) is called.
5. Not Handling Coroutine Exceptions
Unhandled exceptions in coroutines crash the scope. Use CoroutineExceptionHandler or try/catch inside coroutines.
6. Forgetting That StateFlow Always Has a Value
StateFlow requires an initial value and always provides the latest value to new collectors. Use SharedFlow for events.
Practice Questions
1. What is structured concurrency?
Coroutines are organized into scopes. When a scope is cancelled, all its children are cancelled. If a child fails, siblings in coroutineScope are cancelled.
2. What’s the difference between Flow and Channel?
Flow is cold — no code runs until collected. Channel is hot — producers emit regardless of consumers.
3. What does suspend mean?
A suspend function can pause execution without blocking the thread. It resumes later when the result is ready.
4. What is a type-safe DSL builder?
A builder that uses init: Type.() -> Unit to create a receiver scope. Methods are called directly on the receiver.
5. Challenge: Build a Flow that converts a click listener into a stream.
Create a callbackFlow that emits click events. Use awaitClose to clean up the listener.
Mini Project: Reactive Search
fun searchFlow(queryFlow: Flow<String>): Flow<List<String>> {
return queryFlow
.debounce(300) // Wait 300ms after last keystroke
.distinctUntilChanged() // Skip duplicates
.flatMapLatest { query ->
flow {
if (query.length >= 2) {
emit(fetchSearchResults(query))
} else {
emit(emptyList())
}
}
}
}FAQ
What’s Next
Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro