Coroutine, Flow and LiveData — Android Student-Friendly Tutorial

Coroutine, Flow and LiveData — Android Student-Friendly Tutorial

1. Very simple meaning

Coroutine = Do asynchronous work easily.
Flow      = Send multiple values over time.
LiveData  = Lifecycle-aware observable data for UI.

Example:

Coroutine:
Fetch user profile from API.

Flow:
Observe live search results, database changes, upload progress.

LiveData:
Observe data from ViewModel in Activity/Fragment safely.

Android’s coroutine docs say Kotlin coroutines help write asynchronous code and lifecycle-aware components provide scopes such as viewModelScope to manage when coroutines run. (Android Developers)

---

2. Why do we need them?

In Android, we should not do heavy work on the main thread.

Bad:

fun loadUser() {
    val user = api.getUser() // Network call on main thread - bad
    textView.text = user.name
}

Problem:

App freezes
UI becomes slow
ANR may happen

Correct approach:

Use Coroutine for background work.
Use Flow/LiveData to send result to UI.

---

3. Coroutine

What is Coroutine?

A coroutine is a lightweight way to run asynchronous or background work.

Simple example:

viewModelScope.launch {
    val user = repository.getUser()
    _uiState.value = UserUiState(user = user)
}

Meaning:

Start coroutine inside ViewModel.
Call repository.
When result comes, update UI state.

---

Coroutine real-life analogy

You order tea at a shop.

Without coroutine:
You stand and do nothing until tea is ready.

With coroutine:
You order tea, then continue other work.
When tea is ready, you receive it.

---

4. Coroutine important terms

suspend

suspend means:

This function can pause without blocking the thread.

Example:

suspend fun getUser(): User {
    return api.getUser()
}

It does not mean the function runs on background automatically. It means it can be suspended and resumed.

---

launch

Use launch when you do not need to return a result directly.

viewModelScope.launch {
    repository.syncData()
}

Example use cases:

Upload logs
Save data
Call API and update state

---

async

Use async when you want a result later using await().

viewModelScope.launch {
    val profileDeferred = async { repository.getProfile() }
    val ordersDeferred = async { repository.getOrders() }

    val profile = profileDeferred.await()
    val orders = ordersDeferred.await()
}

Use case:

Run two independent API calls in parallel.

---

CoroutineScope

Scope controls coroutine lifecycle.

Example:

viewModelScope.launch {
    // Coroutine cancelled when ViewModel is cleared
}

Common scopes:

ScopeUse
viewModelScopeWork related to ViewModel
lifecycleScopeWork related to Activity/Fragment lifecycle
rememberCoroutineScopeCompose UI event work
Custom scopeLong app-level work, carefully managed

---

Dispatchers

Dispatcher decides which thread coroutine uses.

DispatcherUse
Dispatchers.MainUI work
Dispatchers.IONetwork, database, file
Dispatchers.DefaultCPU-heavy work
Dispatchers.UnconfinedRarely used

Example:

viewModelScope.launch {
    val user = withContext(Dispatchers.IO) {
        repository.getUserFromApi()
    }

    _uiState.value = UserUiState(user = user)
}

---

5. Coroutine pseudocode

WHEN screen opens:

    ViewModel starts coroutine

    show loading

    call API on IO thread

    if success:
        update UI state with data

    if error:
        update UI state with error

    hide loading

Code:

fun loadUser() {
    viewModelScope.launch {
        _uiState.value = UserUiState(isLoading = true)

        try {
            val user = withContext(Dispatchers.IO) {
                repository.getUser()
            }

            _uiState.value = UserUiState(
                isLoading = false,
                user = user
            )
        } catch (e: Exception) {
            _uiState.value = UserUiState(
                isLoading = false,
                error = e.message
            )
        }
    }
}

---

6. Flow

What is Flow?

Flow is a stream of values over time.

Android docs describe Flow as a type that can emit multiple values sequentially, unlike suspend functions that return only one value. (Android Developers)

Example:

Suspend function:
getUser() returns one User.

Flow:
observeUsers() keeps sending updated User list whenever database changes.

---

Flow analogy

Suspend function = one glass of water.
Flow             = water tap, values keep coming.

---

Simple Flow example

fun countDown(): Flow<Int> = flow {
    emit(3)
    delay(1000)

    emit(2)
    delay(1000)

    emit(1)
    delay(1000)

    emit(0)
}

Collect it:

viewModelScope.launch {
    countDown().collect { value ->
        Log.d("Flow", "Value = $value")
    }
}

Output:

3
2
1
0

---

7. Flow important terms

emit

Sends a value from Flow.

emit(userList)

collect

Receives values from Flow.

flow.collect { value ->
    println(value)
}

Cold Flow

Most normal flows are cold.

Meaning:

Flow starts only when someone collects it.
Each collector starts the flow again.

Kotlin docs say regular Flow is usually cold and starts separately for each collector. (Kotlin)

Example:

val flow = flow {
    println("Flow started")
    emit(1)
}
If nobody collects, nothing happens.

---

8. Flow operators

Operators transform Flow data.

map

repository.getProducts()
    .map { products ->
        products.filter { it.isAvailable }
    }

Meaning:

Convert one emitted value into another.

---

filter

flowOf(1, 2, 3, 4)
    .filter { it % 2 == 0 }
    .collect {
        println(it)
    }

Output:

2
4

---

catch

Handle errors.

repository.getProducts()
    .catch { e ->
        emit(emptyList())
    }
    .collect { products ->
        _uiState.value = ProductUiState(products = products)
    }

---

flowOn

Changes dispatcher for upstream Flow work.

repository.getProducts()
    .flowOn(Dispatchers.IO)

Meaning:

Run upstream work on IO thread.

---

9. Flow with Room database

Room supports Flow very nicely.

DAO:

@Dao
interface ProductDao {

    @Query("SELECT * FROM products")
    fun observeProducts(): Flow<List<ProductEntity>>
}

Repository:

class ProductRepository(
    private val dao: ProductDao
) {
    fun observeProducts(): Flow<List<Product>> {
        return dao.observeProducts()
            .map { entities ->
                entities.map { it.toDomain() }
            }
    }
}

ViewModel:

class ProductViewModel(
    private val repository: ProductRepository
) : ViewModel() {

    val products: StateFlow<List<Product>> =
        repository.observeProducts()
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5000),
                initialValue = emptyList()
            )
}

Compose:

@Composable
fun ProductScreen(viewModel: ProductViewModel) {
    val products by viewModel.products.collectAsState()

    LazyColumn {
        items(products) { product ->
            Text(product.name)
        }
    }
}

---

10. StateFlow

What is StateFlow?

StateFlow is a Flow that holds current state.

Example:

Current UI state:
Loading
Success
Error

StateFlow always has a current value. Android docs describe StateFlow and SharedFlow as Flow APIs used to emit state updates and values to multiple consumers. (Android Developers)

---

StateFlow example

data class LoginUiState(
    val isLoading: Boolean = false,
    val userName: String = "",
    val error: String? = null
)

ViewModel:

class LoginViewModel : ViewModel() {

    private val _uiState = MutableStateFlow(LoginUiState())
    val uiState: StateFlow<LoginUiState> = _uiState

    fun login(email: String, password: String) {
        viewModelScope.launch {
            _uiState.value = LoginUiState(isLoading = true)

            try {
                val user = repository.login(email, password)

                _uiState.value = LoginUiState(
                    isLoading = false,
                    userName = user.name
                )
            } catch (e: Exception) {
                _uiState.value = LoginUiState(
                    isLoading = false,
                    error = e.message
                )
            }
        }
    }
}

Compose:

val state by viewModel.uiState.collectAsState()

if (state.isLoading) {
    CircularProgressIndicator()
}

state.error?.let {
    Text(it)
}

---

11. SharedFlow

What is SharedFlow?

SharedFlow is used for one-time events or shared streams.

Example:

Show Toast
Navigate to next screen
Show Snackbar
Open dialog

Kotlin docs describe SharedFlow as a hot Flow that shares emitted values among collectors. (Kotlin)

---

SharedFlow example for navigation

ViewModel:

sealed class LoginEvent {
    object NavigateHome : LoginEvent()
    data class ShowMessage(val message: String) : LoginEvent()
}

class LoginViewModel : ViewModel() {

    private val _event = MutableSharedFlow<LoginEvent>()
    val event = _event.asSharedFlow()

    fun login(email: String, password: String) {
        viewModelScope.launch {
            try {
                repository.login(email, password)
                _event.emit(LoginEvent.NavigateHome)
            } catch (e: Exception) {
                _event.emit(LoginEvent.ShowMessage("Login failed"))
            }
        }
    }
}

Activity/Fragment/Compose collects events:

LaunchedEffect(Unit) {
    viewModel.event.collect { event ->
        when (event) {
            LoginEvent.NavigateHome -> {
                navController.navigate("home")
            }

            is LoginEvent.ShowMessage -> {
                snackbarHostState.showSnackbar(event.message)
            }
        }
    }
}

---

12. StateFlow vs SharedFlow

PointStateFlowSharedFlow
PurposeUI stateEvents
Has current valueYesNot necessary
Needs initial valueYesNo
ExampleLoading/data/error stateToast/navigation
Replays latest valueYes, 1 latest valueConfigurable

Simple memory:

StateFlow = state of screen.
SharedFlow = events happening on screen.

---

13. LiveData

What is LiveData?

LiveData is an observable data holder that is lifecycle-aware.

It means:

Activity/Fragment observes it.
LiveData sends updates only when lifecycle is active.

Android’s LiveData docs describe it as lifecycle-aware observable data holder for UI components. (Android Developers)

---

LiveData example

ViewModel:

class UserViewModel : ViewModel() {

    private val _user = MutableLiveData<User>()
    val user: LiveData<User> = _user

    fun loadUser() {
        viewModelScope.launch {
            val result = repository.getUser()
            _user.value = result
        }
    }
}

Activity:

viewModel.user.observe(this) { user ->
    textView.text = user.name
}

Fragment:

viewModel.user.observe(viewLifecycleOwner) { user ->
    binding.nameText.text = user.name
}

Important in Fragment:

Use viewLifecycleOwner, not this.

---

14. LiveData vs Flow

PointLiveDataFlow
Lifecycle-aware by defaultYesNeeds lifecycle collection
Coroutine-basedNo, but interoperableYes
OperatorsLimitedVery powerful
Best forSimple UI observationStreams, transformations, repository layer
PlatformAndroid onlyKotlin multiplatform/core
Current recommendationGood for legacy/simple appsPreferred in modern Kotlin architecture

Simple memory:

LiveData = Android UI observable.
Flow = Kotlin reactive stream.

---

15. Lifecycle-aware Flow collection

In Fragment:

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state ->
            render(state)
        }
    }
}

Why use repeatOnLifecycle?

It collects only when UI is visible/started.
It stops collection when UI is stopped.
It starts again when UI comes back.

Android lifecycle coroutine docs recommend lifecycle-aware scopes and collection APIs for safe coroutine usage with UI. (Android Developers)

---

16. Coroutine + Flow + LiveData in Clean Architecture

Recommended modern flow:

Data Layer:
Room/API returns Flow or suspend function

Domain Layer:
UseCase exposes Flow or suspend function

Presentation Layer:
ViewModel converts to StateFlow

UI:
Collect StateFlow

Example:

Room DAO
   |
   v
Repository returns Flow<List<Product>>
   |
   v
UseCase returns Flow<List<Product>>
   |
   v
ViewModel exposes StateFlow<ProductUiState>
   |
   v
Compose/Fragment observes UI state

---

17. Scenario-based usage

Scenario 1: Single API call

Requirement:

User opens profile screen.
Fetch profile once.

Use:

Coroutine + suspend function

Example:

fun loadProfile() {
    viewModelScope.launch {
        val profile = repository.getProfile()
        _uiState.value = ProfileUiState(profile = profile)
    }
}

---

Scenario 2: Observe database changes

Requirement:

Show product list and update automatically when database changes.

Use:

Flow

Example:

val products = repository.observeProducts()

---

Scenario 3: UI screen state

Requirement:

Show loading, success, error.

Use:

StateFlow

Example:

private val _uiState = MutableStateFlow(ProductUiState())
val uiState = _uiState.asStateFlow()

---

Scenario 4: Toast/navigation event

Requirement:

After login success, navigate to Home only once.

Use:

SharedFlow

Example:

_event.emit(LoginEvent.NavigateHome)

---

Scenario 5: Old XML project

Requirement:

Simple ViewModel observation in legacy XML app.

Use:

LiveData is okay

Example:

viewModel.user.observe(viewLifecycleOwner) { user ->
    binding.name.text = user.name
}

---

18. Complete mini MVVM example

UI State

data class ProductUiState(
    val isLoading: Boolean = false,
    val products: List<Product> = emptyList(),
    val error: String? = null
)

Repository

class ProductRepository(
    private val api: ProductApi
) {
    suspend fun getProducts(): List<Product> {
        return api.getProducts()
    }
}

ViewModel with Coroutine + StateFlow

class ProductViewModel(
    private val repository: ProductRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow(ProductUiState())
    val uiState = _uiState.asStateFlow()

    fun loadProducts() {
        viewModelScope.launch {
            _uiState.value = ProductUiState(isLoading = true)

            try {
                val products = repository.getProducts()

                _uiState.value = ProductUiState(
                    isLoading = false,
                    products = products
                )
            } catch (e: Exception) {
                _uiState.value = ProductUiState(
                    isLoading = false,
                    error = e.message ?: "Something went wrong"
                )
            }
        }
    }
}

Fragment collection

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
        viewModel.uiState.collect { state ->
            if (state.isLoading) {
                showLoading()
            } else {
                hideLoading()
            }

            showProducts(state.products)

            state.error?.let {
                showError(it)
            }
        }
    }
}

---

19. Common interview questions

Q1. What is coroutine?

A coroutine is a lightweight concurrency tool in Kotlin used to write asynchronous code without blocking threads.

---

Q2. What is suspend function?

A suspend function can pause and resume execution without blocking the thread.

---

Q3. Difference between launch and async?

launch = fire and forget, returns Job.
async  = returns Deferred result, use await().

---

Q4. What is Flow?

Flow is a cold asynchronous stream that can emit multiple values over time.

---

Q5. Flow vs suspend function?

suspend function returns one result.
Flow emits multiple values over time.

---

Q6. StateFlow vs LiveData?

StateFlow is coroutine-based and always has current state.
LiveData is Android lifecycle-aware observable data holder.

---

Q7. StateFlow vs SharedFlow?

StateFlow for UI state.
SharedFlow for one-time events.

---

Q8. Why not use LiveData for repository layer?

Because LiveData is Android-specific. Flow is Kotlin-based, has richer operators, and is better suited for data/domain layers.

---

Q9. What is repeatOnLifecycle?

It safely collects Flow only when lifecycle is at least a given state, commonly STARTED.

---

20. Best memory trick

Coroutine:
"Do work asynchronously."

Flow:
"Send many values over time."

StateFlow:
"Hold current UI state."

SharedFlow:
"Send one-time events."

LiveData:
"Old but useful lifecycle-aware UI observable."

Final one-line answer:

Use Coroutine for async tasks, Flow for continuous data streams, StateFlow for UI state, SharedFlow for events, and LiveData mainly for simple or legacy lifecycle-aware UI observation.