Kotlin coroutines provide a powerful way to handle asynchronous programming, but with great power comes the need for proper understanding. This guide will help you avoid common pitfalls and write better coroutine-based code.
Scope Management
DO: Use Structured Concurrency
Always launch coroutines within a proper scope to ensure proper lifecycle management and automatic cancellation.
class MyViewModel : ViewModel() {
fun loadData() {
viewModelScope.launch {
val data = fetchData()
// Update UI
}
}
}
DON'T: Use GlobalScope
Avoid GlobalScope as it creates coroutines that live for the entire application lifetime and won't be automatically cancelled.
// ❌ Bad practice
GlobalScope.launch {
fetchData()
}
Dispatcher Selection
DO: Choose the Right Dispatcher
Use Dispatchers.IO for network/disk operations, Dispatchers.Default for CPU-intensive work, and Dispatchers.Main for UI updates.
withContext(Dispatchers.IO) {
val data = database.query()
}
withContext(Dispatchers.Default) {
processLargeDataSet(data)
}
DON'T: Block the Main Thread
Never perform blocking operations on the main dispatcher without switching context.
// ❌ Bad - blocks UI thread
viewModelScope.launch(Dispatchers.Main) {
val data = Thread.sleep(1000)
}
Exception Handling
DO: Handle Exceptions Properly
Use try-catch blocks or CoroutineExceptionHandler to handle exceptions gracefully.
viewModelScope.launch {
try {
val result = fetchData()
} catch (e: Exception) {
handleError(e)
}
}
DON'T: Ignore Exceptions
Unhandled exceptions in coroutines can crash your app or cause silent failures.
// ❌ Bad - exception might be lost
launch {
riskyOperation() // May throw exception
}
Cancellation
DO: Make Code Cancellation-Aware
Check for cancellation in long-running operations and use cancellable suspending functions.
withContext(Dispatchers.Default) {
repeat(1000) { i ->
ensureActive() // Check cancellation
processItem(i)
}
}
DON'T: Ignore Cancellation
Don't perform long computations without checking if the coroutine is still active.
// ❌ Bad - won't respond to cancellation
repeat(1000000) { i ->
complexCalculation(i)
}
Flow and LiveData
DO: Use Flow for Streams of Data
Prefer Flow for reactive streams and cold data sources that can be cancelled and transformed.
fun observeUsers(): Flow<List<User>> = flow {
while (true) {
emit(database.getUsers())
delay(5000)
}
}
DON'T: Mix Blocking and Suspending Code
Don't use runBlocking in production code except for main functions and tests.
// ❌ Bad - blocks the thread
fun getData() = runBlocking {
fetchData()
}
Performance
DO: Use async for Parallel Operations
When you need multiple independent operations to run concurrently, use async and await.
val user = async { fetchUser() }
val posts = async { fetchPosts() }
val result = Result(user.await(), posts.await())
DON'T: Create Unnecessary Coroutines
Avoid launching coroutines for operations that are already suspending.
// ❌ Bad - unnecessary launch
launch {
suspendingFunction()
}
// ✓ Good - just call it
suspendingFunction()
Testing
DO: Use TestDispatchers
Use TestCoroutineDispatcher or StandardTestDispatcher for deterministic testing.
@Test
fun testCoroutine() = runTest {
val viewModel = MyViewModel()
viewModel.loadData()
advanceUntilIdle()
assertEquals(expected, viewModel.state)
}
DON'T: Use Real Delays in Tests
Never use actual delays in unit tests as they make tests slow and flaky.
// ❌ Bad - makes tests slow
@Test
fun testWithDelay() {
launch { delay(1000) }
Thread.sleep(1500)
}