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
- Use Flow instead of LiveData – Better performance
- Avoid blocking calls – Use suspend functions
- Use appropriate buffer – For Flow operators
- Cancel unused coroutines – Prevent memory leaks
- 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.