One thing that confused me early on, was understanding the relationship between coroutine context and dispatchers. But once you get it, it makes your Android development much smoother.
What is Coroutine Context?
Think of coroutine context as a map that holds information about your coroutine. It contains elements like the Job, dispatcher, exception handler, and other metadata. Each coroutine has its own context.
// Simple coroutine with context
viewModelScope.launch {
// This coroutine inherits context from viewModelScope
// which includes Dispatchers.Main.immediate
}
The context is inherited from parent to child coroutines. But you can also combine contexts using the +
operator:
val customContext = Dispatchers.IO + SupervisorJob()
launch(customContext) {
// This coroutine runs on IO dispatcher with SupervisorJob
}
Understanding Dispatchers
Dispatchers determine which thread or thread pool your coroutine runs on. Android provides several built-in dispatchers, each optimized for different types of work.
Dispatchers.Main
This runs coroutines on the main UI thread. Use it for UI updates and quick operations.
viewModelScope.launch(Dispatchers.Main) {
textView.text = "Updated on main thread"
progressBar.visibility = View.GONE
}
Dispatchers.IO
Designed for I/O operations like network requests, database queries, and file operations. It uses a shared pool of threads that can grow as needed.
viewModelScope.launch(Dispatchers.IO) {
val response = apiService.getData()
val result = database.insert(response)
withContext(Dispatchers.Main) {
updateUI(result)
}
}
Dispatchers.Default
Best for CPU-intensive work like parsing large JSON, complex calculations, or image processing. It uses a thread pool sized to the number of CPU cores.
viewModelScope.launch(Dispatchers.Default) {
val processedData = heavyDataProcessing(rawData)
withContext(Dispatchers.Main) {
displayProcessedData(processedData)
}
}
Dispatchers.Unconfined
This one is tricky. The coroutine starts in the caller thread but can resume in any thread. Generally avoid this unless you know what you're doing.
// Not recommended for most use cases
launch(Dispatchers.Unconfined) {
println("Before suspend: ${Thread.currentThread().name}")
delay(100)
println("After suspend: ${Thread.currentThread().name}")
}
Custom Dispatchers
Sometimes you need a custom dispatcher for specific requirements:
// Custom dispatcher with limited thread pool
val customDispatcher = Executors.newFixedThreadPool(4).asCoroutineDispatcher()
launch(customDispatcher) {
// Your work here
}
// Don't forget to close it when done
customDispatcher.close()
Switching Context with withContext
One of the most useful patterns is switching contexts within a coroutine:
suspend fun loadUserData(): User {
return withContext(Dispatchers.IO) {
// Network call on IO dispatcher
val userData = userApi.getUser()
// Database operation also on IO
userDatabase.saveUser(userData)
userData
}
// Function returns on the original dispatcher
}
Common Mistakes I've Made
Mistake 1: Using Dispatchers.Main for heavy operations
// Wrong - blocks UI thread
viewModelScope.launch(Dispatchers.Main) {
val result = processLargeFile() // This blocks UI
updateUI(result)
}
// Right - process on Default, update on Main
viewModelScope.launch(Dispatchers.Default) {
val result = processLargeFile()
withContext(Dispatchers.Main) {
updateUI(result)
}
}
Mistake 2: Creating unnecessary context switches
// Inefficient - unnecessary context switches
viewModelScope.launch(Dispatchers.IO) {
withContext(Dispatchers.Default) {
withContext(Dispatchers.IO) {
// Just stay on IO if you're doing I/O work
}
}
}
Testing with Dispatchers
When writing tests, you often need to replace dispatchers:
@ExperimentalCoroutinesApi
class MyViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
@Test
fun testDataLoading() = runTest {
// Test runs on TestDispatcher automatically
viewModel.loadData()
// Assertions here
}
}
The key thing to remember is that coroutine context and dispatchers work together to control where and how your coroutines execute. Choose the right dispatcher for your work type, and don't be afraid to switch contexts when needed.
Most Android development follows this pattern: start UI operations on Main, switch to IO for network/database work, use Default for heavy processing, then switch back to Main for UI updates. Once you get comfortable with this flow, coroutines become much more predictable.
Top comments (0)