What is the best practice to use Kotlin Flow?
Use Flows to represent streams of data that are asynchronous and can change over time. They are the cornerstone of a reactive and lifecycle-aware app.
1. Choosing the Right Type of Flow
| Flow Type | Purpose | Best For |
|---|---|---|
Flow<T> |
Cold stream that emits data when collected. Doesn’t hold state. | Data layer: Streaming results from Room DB, network calls. |
StateFlow<T> |
Hot stream that holds and replays the latest state to collectors. | UI State: Holding the state of a screen (e.g., UiState). Replaces LiveData. |
SharedFlow<T> |
Hot stream that emits values to all collectors. Configurable replay cache. | Events: One-off events like navigation, toast messages, or other signals that should be processed once. |
Best Practice: Use StateFlow for state and SharedFlow for events. This clear separation is a fundamental pattern in modern Android architecture.
// Correct: StateFlow for state
class MyViewModel : ViewModel() {
private val _uiState = MutableStateFlow(MyUiState())
val uiState: StateFlow<MyUiState> = _uiState.asStateFlow()
}
// Correct: SharedFlow for events
private val _navigationEvent = MutableSharedFlow<String>()
val navigationEvent = _navigationEvent.asSharedFlow()
fun onButtonClicked() {
viewModelScope.launch {
_navigationEvent.emit("destination") // Emit an event
}
}
2. Lifecycle-Aware Collection in the UI
This is the most critical best practice. Never collect a flow in the UI without considering the lifecycle.
Use repeatOnLifecycle or flowWithLifecycle. This ensures collection stops when the UI isn’t visible (e.g., in the background), saving resources and preventing crashes.
// In a Fragment's onViewCreated
viewLifecycleOwner.lifecycleScope.launch {
// SAFE: Collection only happens in the STARTED state and above
// and cancels when the lifecycle moves to STOPPED.
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.uiState.collect { state ->
// Update UI with the state
}
}
}
// Alternative using the extension function
viewLifecycleOwner.lifecycleScope.launch {
viewModel.uiState
.flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
.collect { state ->
// Update UI with the state
}
}
NEVER do this: This collects forever, even when the app is in the background.
lifecycleScope.launch {
viewModel.uiState.collect { ... } // BAD! Wastes resources.
}
3. Starting Flows in the ViewModel
Use the viewModelScope to launch flows that are tied to the ViewModel’s lifecycle. When the ViewModel is cleared, the scope is cancelled, and any ongoing work is stopped.
Use stateIn to convert a cold Flow into a hot StateFlow that multiple collectors can share.
class MyViewModel(repository: MyRepository) : ViewModel() {
// Good: Use stateIn to create a StateFlow from a cold flow.
// started = WhileSubscribed(5000) stops the upstream flow after 5s
// of having no subscribers (saves resources).
val uiState: StateFlow<Result<UiState>> = repository.getDataStream()
.map { Result.Success(it) }
.catch { emit(Result.Error(it)) }
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5000),
initialValue = Result.Loading
)
// For one-off events
private val _toastEvent = MutableSharedFlow<String>()
val toastEvent = _toastEvent.asSharedFlow()
fun fetchData() {
viewModelScope.launch {
try {
repository.fetchData()
_toastEvent.emit("Data fetched!")
} catch (e: IOException) {
_toastEvent.emit("Network error!")
}
}
}
}
Key stateIn parameters:
-
SharingStarted.WhileSubscribed(): Best for most UI-related flows. Stops the upstream when there are no active subscribers. -
SharingStarted.Eagerly: Starts immediately and never stops. Use with caution. -
SharingStarted.Lazily: Starts after the first subscriber appears and never stops.
4. Error Handling
Never let an exception break your collection. Always handle errors gracefully within the flow.
val dataFlow: Flow<Result<Data>> = someColdFlow
.map { data ->
// Map your successful data
Result.Success(data)
}
.catch { e ->
// Emit an error state on exceptions in the upstream
emit(Result.Error(e))
}
// Or handle errors in the collector
viewLifecycleOwner.lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
someFlow.collect { value ->
try {
// Process value
} catch (e: Exception) {
// Handle error in collector
}
}
}
}
5. Performance Optimizations
Use Flow operators wisely to control the processing of data.
-
buffer(): Process emissions from a flow concurrently. Useful if the collector is slow.
someFlow
.buffer() // Allows the producer to run in parallel with the collector
.collect { ... }
conflate(): Skip intermediate values if the collector is slow. Only process the latest value. Perfect for UI updates where you don’t need every intermediate state.
scrollFlow
.conflate() // If we can't keep up, just get the latest scroll position
.collect { ... }
flatMapLatest(): Crucial for search. When a new value is emitted, it cancels the previous transformation and starts a new one.
searchQueryFlow
.debounce(300.ms)
.filter { it.length > 2 }
.distinctUntilChanged()
.flatMapLatest { query -> // Cancel previous search on new query
repository.search(query)
}
.collect { results ->
// Update UI with latest results
}
6. Testing
Flows are testable. Use turbine, a powerful testing library for Flows, or the standard runTestcoroutine rule.
// Using Turbine (highly recommended)
@Test
fun `test my flow`() = runTest {
viewModel.uiState.test {
// Assert initial state
assertEquals(Result.Loading, awaitItem())
// Assert success state
val successState = awaitItem() as Result.Success
assertEquals("expected data", successState.data)
// Confirm the flow is closed
awaitComplete()
}
}
Rule of thumb:
-
StateFlow→ UI State -
SharedFlow→ One-time Events -
Operators (
map,filter,combine) → Business logic in ViewModel -
Lifecycle-aware collection → UI Layer
I am a very enthusiastic Android developer to build solid Android apps. I have a keen interest in developing for Android and have published apps to the Google Play Store. I always open to learning new technologies. For any help drop us a line anytime at contact@mobologicplus.com
