← Back to Articles
Android Kotlin Coroutines Flow StateFlow Mobile Development

Kotlin Coroutines and Flow Complete Guide: Asynchronous Programming in Modern Android Development

Learn Kotlin Coroutines and Flow for modern Android development. Understand suspend functions, coroutine scopes, dispatchers, StateFlow, SharedFlow, error handling, and reactive programming with practical examples.

July 2026
30 min read
Written by Engineering Team

Introduction

Modern Android applications perform many operations that should not block the main thread. Network requests, database operations, file processing, and background calculations require efficient asynchronous programming.

Kotlin Coroutines provide a lightweight solution for asynchronous programming by allowing developers to write asynchronous code in a sequential and readable way.

Kotlin Flow extends coroutines by providing reactive streams that emit multiple values over time. Together, Coroutines and Flow are the foundation of modern Android architectures using ViewModel, Room, Retrofit, and Jetpack Compose.

Kotlin Coroutines and Flow architecture
Coroutines handle asynchronous work while Flow manages reactive data streams.

What Are Kotlin Coroutines?

Kotlin Coroutines are a concurrency framework that allows developers to execute long-running operations without blocking the main thread.

Unlike traditional threads, coroutines are lightweight and can run thousands of concurrent operations with minimal overhead.

Threads Coroutines
Heavyweight Lightweight
More memory usage Minimal memory usage
Blocking operations Suspending operations
Harder cancellation Structured cancellation

Why Use Coroutines in Android?

  • Avoid blocking the main UI thread

  • Simplify asynchronous code

  • Handle API requests easily

  • Support cancellation

  • Work naturally with ViewModel

  • Integrate with Jetpack libraries

Suspend Functions

A suspend function is a function that can pause execution without blocking the underlying thread.

When the suspended operation completes, the coroutine continues from where it stopped.

UserRepository.kt
kotlin
              class UserRepository {

    suspend fun getUsers(): List<User> {

        return api.getUsers()

    }

}

            

Coroutine Scope

A CoroutineScope defines the lifetime of coroutines. When the scope is cancelled, all child coroutines are automatically cancelled.

  • viewModelScope

  • lifecycleScope

  • rememberCoroutineScope

  • CoroutineScope

viewModelScope

viewModelScope is the recommended scope for running coroutines inside Android ViewModels.

UserViewModel.kt
kotlin
              class UserViewModel(
    private val repository: UserRepository
) : ViewModel() {


    fun loadUsers() {

        viewModelScope.launch {

            val users =
                repository.getUsers()

        }

    }

}

            

Coroutine Dispatchers

Dispatchers define which thread or thread pool executes coroutine work.

Dispatcher Purpose
Main UI operations
IO Network and database operations
Default CPU intensive work
Unconfined Advanced use cases

Using Dispatchers Correctly

Repository.kt
kotlin
              class UserRepository {


    suspend fun loadUsers()
        : List<User> {


        return withContext(
            Dispatchers.IO
        ) {

            api.getUsers()

        }

    }

}

            

Structured Concurrency

Structured concurrency ensures that coroutines follow a predictable lifecycle. Child coroutines belong to a parent scope and are cancelled together.

  • Automatic cancellation

  • Error propagation

  • Lifecycle awareness

  • Resource management

Coroutine Builders: launch and async

Coroutine builders create and start coroutines. The two most commonly used builders are launch and async.

Builder Returns Usage
launch Job Fire and forget operations
async Deferred Operations returning values

Using launch

launch starts a coroutine that does not return a result. It is commonly used for updating state, saving data, or triggering background operations.

LaunchExample.kt
kotlin
              viewModelScope.launch {

    repository.saveUser(
        user
    )

}

            

Using async and await

async is used when multiple operations should run concurrently and produce results.

AsyncExample.kt
kotlin
              val users = async {

    userRepository.getUsers()

}


val settings = async {

    settingsRepository.getSettings()

}


val result = combine(
    users.await(),
    settings.await()
)

            

Exception Handling in Coroutines

Coroutine errors should be handled explicitly to prevent application crashes and provide better user experiences.

ExceptionExample.kt
kotlin
              viewModelScope.launch {

    try {

        repository.loadData()

    }
    catch(exception: Exception) {

        println(
            exception.message
        )

    }

}

            

CoroutineExceptionHandler

CoroutineExceptionHandler allows centralized handling of uncaught coroutine exceptions.

Handler.kt
kotlin
              val handler =
    CoroutineExceptionHandler {
        _, exception ->

        println(
            exception.message
        )
    }

            

What Is Kotlin Flow?

Flow is a Kotlin API for creating asynchronous streams of data. Unlike suspend functions that return a single value, Flow can emit multiple values over time.

Flow is commonly used for observing database changes, UI state updates, network streams, and reactive application behavior.

Suspend Function Flow
Returns one value Returns multiple values
Single operation Continuous stream
Request-response Reactive updates

Creating a Flow

A Flow can be created using the flow builder.

SimpleFlow.kt
kotlin
              fun numbersFlow() =
    flow {

        emit(1)

        emit(2)

        emit(3)

    }

            

Collecting Flow Values

Flows are cold streams. They do not start emitting values until they are collected.

CollectExample.kt
kotlin
              numbersFlow()
    .collect { value ->

        println(value)

    }

            

Flow Operators

Flow provides many operators for transforming, filtering, and combining data streams.

  • map

  • filter

  • combine

  • flatMapLatest

  • debounce

  • distinctUntilChanged

Using map Operator

MapExample.kt
kotlin
              usersFlow
    .map { users ->

        users.size

    }
    .collect { count ->

        println(count)

    }

            

Using filter Operator

FilterExample.kt
kotlin
              usersFlow
    .filter { user ->

        user.active

    }
    .collect {

        println(it)

    }

            

StateFlow in Android

StateFlow is a special type of Flow designed for representing state. It always contains the latest value and immediately provides it to new collectors.

StateFlow is the recommended replacement for LiveData in many modern Android applications.

StateFlowExample.kt
kotlin
              class UserViewModel
    : ViewModel() {


    private val _state =
        MutableStateFlow(
            emptyList<User>()
        )


    val state =
        _state.asStateFlow()

}

            

StateFlow with Jetpack Compose

Compose integrates naturally with StateFlow because UI automatically updates when the state changes.

ComposeState.kt
kotlin
              @Composable
fun UserScreen(
    viewModel: UserViewModel
) {


    val users by viewModel.state
        .collectAsState()


    UserList(users)

}

            

SharedFlow in Android

SharedFlow is designed for broadcasting events to multiple collectors. It is commonly used for one-time UI events.

  • Navigation events

  • Toast messages

  • Snackbar notifications

  • Analytics events

SharedFlow Example

EventViewModel.kt
kotlin
              private val _events =
    MutableSharedFlow<String>()


val events =
    _events.asSharedFlow()


fun showMessage() {

    viewModelScope.launch {

        _events.emit(
            "Saved successfully"
        )

    }

}

            

Room Database with Flow

Room integrates directly with Flow and can automatically emit updates whenever database content changes.

UserDao.kt
kotlin
              @Query(
    "SELECT * FROM users"
)
fun observeUsers()
    : Flow<List<User>>

            

Retrofit with Coroutines

Retrofit supports suspend functions for modern API communication without callback-based code.

ApiService.kt
kotlin
              interface ApiService {


    @GET("users")
    suspend fun getUsers()
        : List<User>

}

            

Flow Cancellation

One of the biggest advantages of Kotlin Flow is automatic cancellation support. When the collecting coroutine is cancelled, the Flow operation stops automatically.

FlowCancellation.kt
kotlin
              viewModelScope.launch {

    userFlow.collect {

        updateUi(it)

    }

}

            

When the ViewModel is destroyed, viewModelScope is cancelled and the Flow collector stops running.

Combining Multiple Flows

Real applications often need data from multiple sources. Flow provides operators that allow combining multiple streams together.

CombineFlows.kt
kotlin
              val userFlow =
    userRepository.user


val settingsFlow =
    settingsRepository.settings


combine(
    userFlow,
    settingsFlow
) { user, settings ->

    UserScreenState(
        user,
        settings
    )

}

            

flatMapLatest Operator

flatMapLatest is commonly used for search functionality because it automatically cancels previous requests when a new value arrives.

SearchExample.kt
kotlin
              searchQuery
    .flatMapLatest { query ->

        repository.search(query)

    }
    .collect {

        updateResults(it)

    }

            

Debounce for Search Optimization

The debounce operator waits until the user stops typing before executing an operation. It prevents unnecessary API requests.

DebounceExample.kt
kotlin
              searchText
    .debounce(500)
    .collect { query ->

        searchUsers(query)

    }

            

Repository Pattern with Coroutines and Flow

A common Android architecture approach is to expose Flow from repositories and allow ViewModels to transform data into UI state.

UserRepository.kt
kotlin
              class UserRepository(
    private val api: ApiService,
    private val database: UserDao
) {


    fun users():

        Flow<List<User>> {

        return database
            .observeUsers()

    }

}

            

Creating UI State with Coroutines

A production Android application should represent UI states explicitly instead of manually controlling individual UI components.

UiState.kt
kotlin
              sealed interface UiState {


    data object Loading
        : UiState


    data class Success(
        val users: List<User>
    ) : UiState


    data class Error(
        val message: String
    ) : UiState

}

            

Testing Kotlin Coroutines

Coroutine-based code should be tested using Kotlin coroutine testing tools. Tests should control coroutine execution instead of waiting for real delays.

  • runTest

  • TestDispatcher

  • TestScope

  • Virtual time

Coroutine Test Example

ViewModelTest.kt
kotlin
              @Test
fun loadUsersTest() = runTest {


    viewModel.loadUsers()


    assertEquals(
        5,
        viewModel.state.value.size
    )

}

            

Best Practices for Coroutines

  • Use suspend functions for one-time operations

  • Use Flow for continuous updates

  • Avoid GlobalScope

  • Use lifecycle-aware scopes

  • Handle exceptions properly

  • Keep business logic outside UI

💡 Android Recommendation

Prefer viewModelScope for ViewModel operations because it automatically cancels work when the ViewModel is destroyed.

Common Coroutine Mistakes

  • Running network calls on the Main dispatcher

  • Ignoring cancellation

  • Using blocking code inside coroutines

  • Collecting Flow incorrectly

  • Creating too many collectors

  • Using GlobalScope

Coroutines vs Traditional Callbacks

Callbacks Coroutines
Nested callback code Sequential readable code
Manual error handling Structured exceptions
Hard cancellation Automatic cancellation
More boilerplate Cleaner syntax

Modern Android Architecture with Coroutines and Flow

The recommended architecture for modern Android applications combines Jetpack Compose, ViewModel, Coroutines, Flow, repositories, and clean separation of responsibilities.

Android architecture using Coroutines and Flow
Modern Android architecture with reactive data flow.
  • Compose collects UI state

  • ViewModel manages state

  • Use cases contain business logic

  • Repositories provide data

  • Flow streams updates

Frequently Asked Questions

Are Kotlin Coroutines replacing threads?
Coroutines do not replace threads. They provide a lightweight way to manage asynchronous work using threads more efficiently.
Should I use LiveData or StateFlow?
For new Android applications, StateFlow is usually preferred because it integrates better with Kotlin and modern Compose architecture.
When should I use Flow instead of suspend functions?
Use suspend functions for operations that return one result. Use Flow when data can change over time and multiple values need to be emitted.
Can Room and Retrofit work with Flow?
Yes. Room supports Flow for observing database changes, and Retrofit supports suspend functions for API requests.
Are Coroutines only for Android?
No. Kotlin Coroutines are a general Kotlin feature and can be used in backend applications, desktop applications, and multiplatform projects.

Conclusion

Kotlin Coroutines and Flow are essential technologies for modern Android development. They provide a clean and efficient approach for asynchronous programming, reactive data handling, and scalable application architecture. Combined with Jetpack Compose, ViewModel, and Clean Architecture, they enable developers to build responsive and maintainable Android applications.

We use cookies to improve your experience.