Kotlin Coroutines have revolutionized asynchronous programming in Android. In 2026, coroutines are the standard for handling background tasks, network calls, and database operations in Android apps.

This comprehensive guide covers everything you need to master Kotlin Coroutines for Android development.

Why Kotlin Coroutines?

Advantages Over Traditional Approaches:

  • Simpler Code – Sequential-looking async code
  • Lightweight – Thousands of coroutines on one thread
  • Built-in Cancellation – Automatic cleanup
  • Structured Concurrency – No leaked coroutines
  • Exception Handling – Try-catch works naturally
  • Jetpack Integration – ViewModelScope, lifecycleScope

Basic Coroutine Concepts

1. Suspend Functions

suspend fun fetchUserData(userId: String): User {
    delay(1000) // Simulates network call
    return userRepository.getUser(userId)
}

// Call from coroutine
lifecycleScope.launch {
    val user = fetchUserData("123")
    updateUI(user)
}

2. Coroutine Builders

launch – Fire and forget:

lifecycleScope.launch {
    // Runs in background
    val data = fetchData()
    updateUI(data)
}

async – Returns a result:

val deferred = lifecycleScope.async {
    fetchData()
}

val result = deferred.await()

withContext – Switch context:

suspend fun loadData(): Data {
    return withContext(Dispatchers.IO) {
        // Runs on IO thread
        database.getData()
    }
}

Dispatchers

Main Dispatcher Types:

  • Dispatchers.Main – UI thread
  • Dispatchers.IO – Network/disk operations
  • Dispatchers.Default – CPU-intensive work
  • Dispatchers.Unconfined – Not recommended for general use

Usage Example:

lifecycleScope.launch {
    // Runs on Main thread
    showLoading()
    
    val data = withContext(Dispatchers.IO) {
        // Runs on IO thread
        api.fetchData()
    }
    
    // Back on Main thread
    hideLoading()
    displayData(data)
}

Coroutine Scopes

1. ViewModelScope

class MyViewModel : ViewModel() {
    fun loadData() {
        viewModelScope.launch {
            // Automatically cancelled when ViewModel is cleared
            val data = repository.getData()
            _uiState.value = UiState.Success(data)
        }
    }
}

2. LifecycleScope

class MyActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        lifecycleScope.launch {
            // Cancelled when activity is destroyed
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                // Only runs when activity is started
                viewModel.uiState.collect { state ->
                    updateUI(state)
                }
            }
        }
    }
}

3. Custom Scope

class MyRepository {
    private val scope = CoroutineScope(
        SupervisorJob() + Dispatchers.IO
    )
    
    fun fetchData() {
        scope.launch {
            // Your code here
        }
    }
    
    fun cleanup() {
        scope.cancel()
    }
}

Error Handling

Try-Catch in Coroutines

lifecycleScope.launch {
    try {
        val data = withContext(Dispatchers.IO) {
            api.fetchData()
        }
        updateUI(data)
    } catch (e: IOException) {
        showError("Network error")
    } catch (e: Exception) {
        showError("Unknown error")
    }
}

CoroutineExceptionHandler

val exceptionHandler = CoroutineExceptionHandler { _, exception ->
    Log.e("Coroutine", "Error: ${exception.message}")
}

lifecycleScope.launch(exceptionHandler) {
    // If exception occurs, handler will catch it
    fetchData()
}

Kotlin Flow

What is Flow?

Flow is a cold asynchronous stream that emits values sequentially.

Basic Flow Example:

fun getUsers(): Flow<List<User>> = flow {
    emit(emptyList()) // Initial state
    delay(1000)
    val users = api.fetchUsers()
    emit(users) // Emit result
}

// Collect in Activity/Fragment
lifecycleScope.launch {
    viewModel.users.collect { users ->
        updateUI(users)
    }
}

Flow Operators:

userRepository.getUsers()
    .map { users -> users.filter { it.isActive } }
    .distinctUntilChanged()
    .catch { e -> emit(emptyList()) }
    .flowOn(Dispatchers.IO)
    .collect { users ->
        updateUI(users)
    }

StateFlow and SharedFlow

StateFlow – Hot stream with state:

class MyViewModel : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState: StateFlow<UiState> = _uiState.asStateFlow()
    
    fun loadData() {
        viewModelScope.launch {
            try {
                val data = repository.getData()
                _uiState.value = UiState.Success(data)
            } catch (e: Exception) {
                _uiState.value = UiState.Error(e.message)
            }
        }
    }
}

SharedFlow – Hot stream for events:

private val _events = MutableSharedFlow<Event>()
val events: SharedFlow<Event> = _events.asSharedFlow()

fun sendEvent(event: Event) {
    viewModelScope.launch {
        _events.emit(event)
    }
}

Parallel Execution

Using async for Parallel Calls:

suspend fun loadData() {
    coroutineScope {
        val user = async { api.fetchUser() }
        val posts = async { api.fetchPosts() }
        val comments = async { api.fetchComments() }
        
        // Wait for all to complete
        val userData = user.await()
        val postsData = posts.await()
        val commentsData = comments.await()
        
        combineData(userData, postsData, commentsData)
    }
}

Cancellation

Cooperative Cancellation:

val job = lifecycleScope.launch {
    repeat(1000) { i ->
        if (!isActive) return@launch // Check cancellation
        delay(100)
        updateProgress(i)
    }
}

// Cancel when needed
job.cancel()

withTimeout:

try {
    withTimeout(5000) {
        val data = api.fetchData()
        updateUI(data)
    }
} catch (e: TimeoutCancellationException) {
    showError("Request timed out")
}

Testing Coroutines

Test Dispatcher:

@Test
fun testViewModel() = runTest {
    val viewModel = MyViewModel(fakeRepository)
    
    viewModel.loadData()
    advanceUntilIdle()
    
    assertEquals(UiState.Success, viewModel.uiState.value)
}

Testing Flow:

@Test
fun testFlow() = runTest {
    val flow = repository.getUsers()
    
    flow.test {
        assertEquals(emptyList(), awaitItem())
        assertEquals(testUsers, awaitItem())
        awaitComplete()
    }
}

Best Practices

1. Use Appropriate Scope

  • ViewModelScope for ViewModel operations
  • LifecycleScope for UI operations
  • Custom scope for repositories

2. Choose Right Dispatcher

  • Dispatchers.IO for network/database
  • Dispatchers.Default for CPU work
  • Dispatchers.Main for UI updates

3. Handle Errors Properly

viewModelScope.launch {
    try {
        val data = withContext(Dispatchers.IO) {
            repository.getData()
        }
        _uiState.value = UiState.Success(data)
    } catch (e: Exception) {
        _uiState.value = UiState.Error(e.message ?: "Unknown error")
    }
}

4. Use Structured Concurrency

suspend fun loadAllData() = coroutineScope {
    // All child coroutines must complete
    launch { loadUsers() }
    launch { loadPosts() }
    // Function won't return until all complete
}

5. Avoid GlobalScope

// BAD - Hard to cancel, can leak
GlobalScope.launch {
    fetchData()
}

// GOOD - Tied to lifecycle
viewModelScope.launch {
    fetchData()
}

Common Patterns

Repository Pattern:

class UserRepository(
    private val api: ApiService,
    private val dao: UserDao
) {
    fun getUsers(): Flow<List<User>> = flow {
        // Emit cached data first
        emit(dao.getUsers())
        
        // Fetch fresh data
        val freshData = api.fetchUsers()
        dao.insertUsers(freshData)
        
        // Emit fresh data
        emit(freshData)
    }.flowOn(Dispatchers.IO)
}

ViewModel Pattern:

class MyViewModel(
    private val repository: UserRepository
) : ViewModel() {
    
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading)
    val uiState = _uiState.asStateFlow()
    
    init {
        loadUsers()
    }
    
    private fun loadUsers() {
        viewModelScope.launch {
            repository.getUsers()
                .catch { e ->
                    _uiState.value = UiState.Error(e.message)
                }
                .collect { users ->
                    _uiState.value = UiState.Success(users)
                }
        }
    }
}

Performance Tips

  1. Use Flow instead of LiveData – Better performance
  2. Avoid blocking calls – Use suspend functions
  3. Use appropriate buffer – For Flow operators
  4. Cancel unused coroutines – Prevent memory leaks
  5. Use conflate() – For fast producers

Conclusion

Kotlin Coroutines are essential for modern Android development. They make asynchronous programming simple, safe, and efficient. By mastering coroutines, you can write cleaner, more maintainable code.

Start with basic coroutines and gradually adopt Flow, StateFlow, and advanced patterns. The investment in learning coroutines will pay off in better app performance and code quality.

For more Android development tips, check out our free WebP converter tool.