Skip to content
Kotlin Advanced: Coroutines, DSLs & Multiplatform

Kotlin Advanced: Coroutines, DSLs & Multiplatform

DodaTech Updated Jun 20, 2026 7 min read

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

Are Kotlin coroutines the same as threads?
No. Coroutines are suspendable computations. Thousands of coroutines can run on a few threads. They’re much lighter than threads.
Can I use Kotlin for iOS development?
Yes — Kotlin Multiplatform shares business logic across Android, iOS, web, and desktop. UI is still platform-specific (Jetpack Compose for Android, SwiftUI for iOS).
What is kotlinx.serialization?
A multiplatform serialization library that converts Kotlin objects to JSON, CBOR, ProtoBuf, and other formats. Uses @Serializable annotations.

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