Back to blog
July 24, 2025
3 min read

Working with Coroutines in Kotlin

Learn how to write asynchronous code efficiently using Kotlin coroutines

Working with Coroutines in Kotlin

Coroutines are Kotlin’s solution for writing asynchronous code in a sequential manner. They help you write clean, efficient, and maintainable asynchronous code without the complexity of traditional callback-based approaches.

Basic Coroutines

Launching a Coroutine

// Using GlobalScope (not recommended for production)
GlobalScope.launch {
    println("Coroutine is running")
}

// Using CoroutineScope
class MyClass {
    private val scope = CoroutineScope(Dispatchers.Main)

    fun startCoroutine() {
        scope.launch {
            println("Coroutine is running")
        }
    }
}

Coroutine Builders

// launch - fire and forget
scope.launch {
    // Do something
}

// async - returns a result
val result = scope.async {
    // Do something and return a value
    "Result"
}.await()

// runBlocking - blocks the current thread
runBlocking {
    // Do something
}

Coroutine Context

Dispatchers

// Main thread (UI)
launch(Dispatchers.Main) {
    // Update UI
}

// IO operations
launch(Dispatchers.IO) {
    // Network calls, file operations
}

// CPU-intensive work
launch(Dispatchers.Default) {
    // Complex calculations
}

// Custom dispatcher
val customDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()

Job and SupervisorJob

// Regular job
val job = scope.launch {
    // Coroutine work
}

// Supervisor job
val supervisorJob = SupervisorJob()
val scope = CoroutineScope(supervisorJob)

Coroutine Scope

ViewModelScope

class MyViewModel : ViewModel() {
    fun fetchData() {
        viewModelScope.launch {
            // Coroutine work
        }
    }
}

LifecycleScope

class MyActivity : AppCompatActivity() {
    fun startWork() {
        lifecycleScope.launch {
            // Coroutine work
        }
    }
}

Structured Concurrency

Parent-Child Relationship

scope.launch {
    // Parent coroutine
    launch {
        // Child coroutine
    }
}

Cancellation

val job = scope.launch {
    try {
        // Coroutine work
    } catch (e: CancellationException) {
        // Handle cancellation
    }
}

// Cancel the coroutine
job.cancel()

Exception Handling

Try-Catch

scope.launch {
    try {
        // Risky operation
    } catch (e: Exception) {
        // Handle error
    }
}

CoroutineExceptionHandler

val handler = CoroutineExceptionHandler { _, exception ->
    println("Caught $exception")
}

scope.launch(handler) {
    // Coroutine work
}

Common Use Cases

Network Calls

suspend fun fetchUser(id: String): User {
    return withContext(Dispatchers.IO) {
        api.getUser(id)
    }
}

Parallel Operations

val results = coroutineScope {
    val users = async { fetchUsers() }
    val posts = async { fetchPosts() }

    Pair(users.await(), posts.await())
}

Timeout

withTimeout(5000L) {
    // Operation that should complete within 5 seconds
}

Best Practices

  1. Use appropriate dispatchers

    // Good
    withContext(Dispatchers.IO) {
        // IO operation
    }
    
    // Avoid
    withContext(Dispatchers.Main) {
        // IO operation
    }
    
  2. Handle exceptions properly

    // Good
    try {
        // Risky operation
    } catch (e: Exception) {
        // Handle error
    }
    
    // Avoid
    // Ignoring exceptions
    
  3. Use structured concurrency

    // Good
    coroutineScope {
        launch { /* ... */ }
        launch { /* ... */ }
    }
    
    // Avoid
    GlobalScope.launch { /* ... */ }
    

Advanced Features

Flow

val flow = flow {
    emit(1)
    emit(2)
    emit(3)
}

scope.launch {
    flow.collect { value ->
        println(value)
    }
}

Channel

val channel = Channel<Int>()
scope.launch {
    channel.send(1)
    channel.send(2)
    channel.close()
}

scope.launch {
    for (value in channel) {
        println(value)
    }
}

Conclusion

Coroutines in Kotlin help you:

  • Write clean asynchronous code
  • Handle complex operations efficiently
  • Manage resources properly
  • Improve code readability

Remember:

  • Use appropriate dispatchers
  • Handle exceptions properly
  • Follow structured concurrency
  • Consider cancellation

Stay tuned for our next post where we’ll explore Kotlin Flow for reactive programming!