Skip to main content

πŸŽ›οΈ Context & Dispatcher

πŸ“– What is CoroutineContext?​

CoroutineContext is a set of configurations that determines how a coroutine will execute. It specifies which thread to run on, what name to use, and more!

πŸ’‘ Dispatcher​

Basic Dispatchers​

fun main() = runBlocking {
// Main - UI thread (Android/Desktop)
launch(Dispatchers.Main) {
// UI updates
}

// IO - Network/file operations
launch(Dispatchers.IO) {
println("IO: ${Thread.currentThread().name}")
}

// Default - CPU-intensive tasks
launch(Dispatchers.Default) {
println("Default: ${Thread.currentThread().name}")
}

// Unconfined - Special cases
launch(Dispatchers.Unconfined) {
println("Unconfined: ${Thread.currentThread().name}")
}

delay(100)
}

Dispatchers.IO​

suspend fun readFile(): String = withContext(Dispatchers.IO) {
// File reading, network requests, etc.
delay(1000)
"File contents"
}

suspend fun writeFile(content: String) = withContext(Dispatchers.IO) {
// File writing
delay(500)
println("File saved: $content")
}

fun main() = runBlocking {
val content = readFile()
writeFile(content)
}

Dispatchers.Default​

suspend fun heavyComputation(): Int = withContext(Dispatchers.Default) {
// CPU-intensive computation
var result = 0
repeat(1_000_000) {
result += it
}
result
}

fun main() = runBlocking {
val result = heavyComputation()
println("Computation result: $result")
}

🎯 Practical Examples​

Dispatcher by Layer​

// Repository - IO
class UserRepository {
suspend fun fetchUser(id: String): User = withContext(Dispatchers.IO) {
delay(1000) // Network request
User(id, "John Doe")
}
}

// UseCase - Default
class ProcessUserUseCase {
suspend fun process(user: User): ProcessedUser = withContext(Dispatchers.Default) {
// Data processing
delay(500)
ProcessedUser(user.name.uppercase())
}
}

data class User(val id: String, val name: String)
data class ProcessedUser(val displayName: String)

fun main() = runBlocking {
val repo = UserRepository()
val useCase = ProcessUserUseCase()

val user = repo.fetchUser("123")
val processed = useCase.process(user)
println("Result: ${processed.displayName}")
}

Parallel IO Operations​

suspend fun loadAllData(): Triple<String, String, String> = coroutineScope {
val user = async(Dispatchers.IO) {
delay(1000)
"User data"
}

val posts = async(Dispatchers.IO) {
delay(1500)
"Posts data"
}

val comments = async(Dispatchers.IO) {
delay(800)
"Comments data"
}

Triple(user.await(), posts.await(), comments.await())
}

fun main() = runBlocking {
val time = measureTimeMillis {
val (user, posts, comments) = loadAllData()
println("$user, $posts, $comments")
}
println("Time elapsed: ${time}ms") // ~1500ms (parallel)
}

πŸ”§ Context Composition​

Naming​

fun main() = runBlocking {
launch(CoroutineName("Task1")) {
println("Name: ${coroutineContext[CoroutineName]}")
}

launch(Dispatchers.IO + CoroutineName("IOTask")) {
println("Thread: ${Thread.currentThread().name}")
println("Name: ${coroutineContext[CoroutineName]}")
}

delay(100)
}

Adding Job​

fun main() = runBlocking {
val job = Job()

launch(job + Dispatchers.Default) {
println("Task running")
delay(1000)
println("Task completed")
}

delay(500)
println("Canceling task")
job.cancel()
}

🎨 withContext​

Thread Switching​

suspend fun complexTask() {
println("Start: ${Thread.currentThread().name}")

// IO operation
val data = withContext(Dispatchers.IO) {
println("IO: ${Thread.currentThread().name}")
"Data"
}

// CPU operation
val processed = withContext(Dispatchers.Default) {
println("Default: ${Thread.currentThread().name}")
data.uppercase()
}

println("End: ${Thread.currentThread().name}")
println("Result: $processed")
}

fun main() = runBlocking {
complexTask()
}

Optimization Pattern​

// ❌ Unnecessary switching
suspend fun bad() {
withContext(Dispatchers.IO) {
val data1 = loadData1()
withContext(Dispatchers.Default) { // Unnecessary!
process(data1)
}
}
}

// βœ… Efficient
suspend fun good() {
val data1 = withContext(Dispatchers.IO) {
loadData1()
}

withContext(Dispatchers.Default) {
process(data1)
}
}

suspend fun loadData1() = delay(100)
suspend fun process(data: Unit) = delay(100)

πŸ”₯ Practical Patterns​

Cache + Network​

class DataSource {
private var cache: String? = null

suspend fun getData(): String {
// Check cache (fast)
cache?.let { return it }

// Network request (slow)
return withContext(Dispatchers.IO) {
delay(1000)
"New data"
}.also { cache = it }
}
}

fun main() = runBlocking {
val source = DataSource()

// First call - Network
val time1 = measureTimeMillis {
println(source.getData())
}
println("First call: ${time1}ms")

// Second call - Cache
val time2 = measureTimeMillis {
println(source.getData())
}
println("Second call: ${time2}ms")
}

Batch Processing​

suspend fun processBatch(items: List<Int>): List<Int> {
return withContext(Dispatchers.Default) {
items.map { item ->
// Process each item
item * 2
}
}
}

fun main() = runBlocking {
val items = List(100) { it }
val results = processBatch(items)
println("Processing completed: ${results.size} items")
}

With Timeout​

suspend fun fetchWithTimeout(): String? {
return try {
withTimeout(2000) {
withContext(Dispatchers.IO) {
delay(3000) // Takes too long
"Data"
}
}
} catch (e: TimeoutCancellationException) {
null
}
}

fun main() = runBlocking {
val result = fetchWithTimeout()
println("Result: ${result ?: "Timeout"}")
}

πŸ›‘οΈ Exception Handling​

CoroutineExceptionHandler​

fun main() = runBlocking {
val handler = CoroutineExceptionHandler { _, exception ->
println("Error handled: ${exception.message}")
}

val job = launch(handler) {
throw Exception("Problem occurred!")
}

job.join()
println("Continues execution")
}

SupervisorJob​

fun main() = runBlocking {
val supervisor = SupervisorJob()

with(CoroutineScope(coroutineContext + supervisor)) {
val job1 = launch {
delay(500)
throw Exception("Task1 failed")
}

val job2 = launch {
delay(1000)
println("Task2 succeeded!")
}

try {
job1.join()
} catch (e: Exception) {
println("Task1 exception: ${e.message}")
}

job2.join()
}
}

🎯 Custom Dispatcher​

Specifying Thread Pool Size​

fun main() = runBlocking {
val customDispatcher = Dispatchers.IO.limitedParallelism(2)

repeat(5) { i ->
launch(customDispatcher) {
println("Task $i: ${Thread.currentThread().name}")
delay(1000)
}
}

delay(3000)
}

Single Thread​

fun main() = runBlocking {
val singleThread = Dispatchers.Default.limitedParallelism(1)

repeat(3) { i ->
launch(singleThread) {
println("Task $i: ${Thread.currentThread().name}")
delay(500)
}
}

delay(2000)
}

πŸ€” Frequently Asked Questions​

Q1. Which Dispatcher should I use?​

A: Choose based on the type of task!

// IO - Network, files, databases
suspend fun fetchData() = withContext(Dispatchers.IO) { }

// Default - CPU-intensive computations
suspend fun compute() = withContext(Dispatchers.Default) { }

// Main - UI updates (Android/Desktop)
suspend fun updateUI() = withContext(Dispatchers.Main) { }

Q2. Can I use withContext multiple times?​

A: Yes! Switch whenever needed.

suspend fun workflow() {
val data = withContext(Dispatchers.IO) {
loadFromNetwork()
}

val processed = withContext(Dispatchers.Default) {
processData(data)
}

withContext(Dispatchers.Main) {
updateUI(processed)
}
}

Q3. What if I don't specify a Dispatcher?​

A: It inherits the parent coroutine's Context!

fun main() = runBlocking(Dispatchers.Default) {
launch { // Inherits Dispatchers.Default
println(Thread.currentThread().name)
}
}

🎬 Conclusion​

Control your coroutines with Context and Dispatcher!

Key Takeaways:
βœ… Dispatchers.IO - Network/files
βœ… Dispatchers.Default - CPU tasks
βœ… Dispatchers.Main - UI updates
βœ… Switch threads with withContext
βœ… Fine-grained control with Context composition

Congratulations! You've completed the Coroutines series! πŸŽ‰

Next Step: Learn how to write tests in Unit Testing!