Anti-Pattern: Stop Leaking Kotlin Flow Collectors in Your Android ViewModel 🤔
Using Kotlin Flows in Android ViewModels is powerful for managing data updates, but improper usage of Flow.collect()
can lead to memory leaks and performance issues.
The Collector Leak Problem in ViewModels
Using Flow.collect()
directly in a ViewModel can create multiple collectors that are not properly released, leading to memory leaks.
Consider this example:
class RefreshableViewModel(private val dataSource: DataSource) : ViewModel() {
private val _viewState = MutableStateFlow<ViewState>(ViewState.Loading)
val viewState: StateFlow<ViewState> = _viewState
init {
reloadData()
}
// Reloads data from the data source. Can be triggered from the UI.
fun reloadData() {
viewModelScope.launch {
dataSource.fetchDataFlow().collect { newData ->
_viewState.emit(newData.toViewState())
}
}
}
}
In this implementation, every call to reloadData()
creates a new collector. If called multiple times (e.g., by a "pull to refresh" action), each call adds a new collector that remains active until the ViewModel is destroyed, resulting in leaked collectors.
Why Do Collectors Leak?
Collectors leak because they remain active indefinitely:
- Finite Flows: If a Flow emits a finite number of values and completes, collectors are eventually garbage collected. This situation is less concerning.
- Hot Flows: Collecting from a hot Flow (e.g., one that listens for continuous updates from a database or SharedPreferences) never ends, meaning collectors are never released. Multiple active collectors consume more memory and degrade performance.
How to Fix Leaked Collectors?
To avoid leaking collectors, use Kotlin Flow operators that safely manage data collection:
➤ Basic Scenario: Use the stateIn()
operator to convert a Flow from the data source into a StateFlow. This method prevents unnecessary data collection by only collecting when the UI is observing.
class SimpleViewModel(private val dataSource: DataSource) : ViewModel() {
val viewState: StateFlow<ViewState> = dataSource.fetchDataFlow()
.map { data -> data.toViewState() }
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), ViewState.Loading)
}
➤ Refresh Scenario: Use MutableSharedFlow
with flatMapLatest()
for cases requiring data refresh (like a refresh button). This approach ensures only the most recent collection is active.
class ReloadableViewModel(private val dataSource: DataSource) : ViewModel() {
private val reloadTrigger = MutableSharedFlow<Unit>(replay = 1)
val viewState: StateFlow<ViewState> = reloadTrigger.flatMapLatest {
dataSource.fetchDataFlow().map { it.toViewState() }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), ViewState())
init {
reloadData()
}
fun reloadData() {
viewModelScope.launch {
reloadTrigger.emit(Unit)
}
}
}
Conclusion
To prevent memory leaks and improve app performance, avoid using Flow.collect()
directly in ViewModels. Instead, use safer Flow operators like stateIn
and flatMapLatest
to manage data collection effectively.
LinkedIn: https://www.linkedin.com/in/sachankapil
GitHub: https://github.com/SachanKapil
YouTube: https://www.youtube.com/@codewithkapil
Telegram Channel: https://t.me/+QkKKJTuNY89hOGRl