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 happenCorrect 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:
| Scope | Use |
|---|---|
viewModelScope | Work related to ViewModel |
lifecycleScope | Work related to Activity/Fragment lifecycle |
rememberCoroutineScope | Compose UI event work |
| Custom scope | Long app-level work, carefully managed |
---
Dispatchers
Dispatcher decides which thread coroutine uses.
| Dispatcher | Use |
|---|---|
Dispatchers.Main | UI work |
Dispatchers.IO | Network, database, file |
Dispatchers.Default | CPU-heavy work |
Dispatchers.Unconfined | Rarely 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 loadingCode:
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
ErrorStateFlow 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 dialogKotlin 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
| Point | StateFlow | SharedFlow |
|---|---|---|
| Purpose | UI state | Events |
| Has current value | Yes | Not necessary |
| Needs initial value | Yes | No |
| Example | Loading/data/error state | Toast/navigation |
| Replays latest value | Yes, 1 latest value | Configurable |
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
| Point | LiveData | Flow |
|---|---|---|
| Lifecycle-aware by default | Yes | Needs lifecycle collection |
| Coroutine-based | No, but interoperable | Yes |
| Operators | Limited | Very powerful |
| Best for | Simple UI observation | Streams, transformations, repository layer |
| Platform | Android only | Kotlin multiplatform/core |
| Current recommendation | Good for legacy/simple apps | Preferred 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 StateFlowExample:
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 functionExample:
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:
FlowExample:
val products = repository.observeProducts()---
Scenario 3: UI screen state
Requirement:
Show loading, success, error.Use:
StateFlowExample:
private val _uiState = MutableStateFlow(ProductUiState())
val uiState = _uiState.asStateFlow()---
Scenario 4: Toast/navigation event
Requirement:
After login success, navigate to Home only once.Use:
SharedFlowExample:
_event.emit(LoginEvent.NavigateHome)---
Scenario 5: Old XML project
Requirement:
Simple ViewModel observation in legacy XML app.Use:
LiveData is okayExample:
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.