'Flow onEach/collect gets called multiple times when back from Fragment
I'm using Flow instead of LiveData to collect data in my Fragment. In Fragment A I observe (or rather collect) the data in my fragment`s onViewCreated like this:
lifecycleScope.launchWhenStarted {
availableLanguagesFlow.collect {
languagesAdapter.setItems(it.allItems, it.selectedItem)
}
}
Problem. Then when I go to Fragment B and then comes back to Fragment A, my collect function gets called twice. If I go the Fragment B again and back to A - then collect function is called 3 times. And so on.
Solution 1:[1]
Reason
It happens because of tricky Fragment lifecycle. When you come back from Fragment B to Fragment A, then Fragment A gets reattached. As a result fragment's onViewCreated gets called second time and you observe the same instance of Flow second time. Other words, now you have one Flow with two observers, and when the flow emits data, then two of them are called.
Solution 1 for Fragment
Use viewLifecycleOwner in Fragment's onViewCreated. To be more specific use viewLifecycleOwner.lifecycleScope.launch instead of lifecycleScope.launch. Like this:
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
availableLanguagesFlow.collect {
languagesAdapter.setItems(it.allItems, it.selectedItem)
}
}
Solution 2 for Activity
In Activity you can simply collect data in onCreate.
lifecycleScope.launchWhenStarted {
availableLanguagesFlow.collect {
languagesAdapter.setItems(it.allItems, it.selectedItem)
}
}
Additional info
- Same happens for LiveData. See the post here. Also check this article.
- Make code cleaner with Kotlin extension:
extension:
fun <T> Flow<T>.launchWhenStarted(lifecycleOwner: LifecycleOwner) {
lifecycleOwner.lifecycleScope.launchWhenStarted {
[email protected]()
}
}
in fragment onViewCreated:
availableLanguagesFlow
.onEach {
//update view
}.launchWhenStarted(viewLifecycleOwner)
Update
I'd rather use now repeatOnLifecycle, because it cancels the ongoing coroutine when the lifecycle falls below the state (onStop in my case). While without repeatOnLifecycle, the collection will be suspended when onStop. Check out this article.
fun <T> Flow<T>.launchWhenStarted(lifecycleOwner: LifecycleOwner)= with(lifecycleOwner) {
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED){
try {
[email protected]()
}catch (t: Throwable){
loge(t)
}
}
}
}
Solution 2:[2]
Use SharedFlow and apply replayCache to it.
Resets the replayCache of this shared flow to an empty state. New subscribers will be receiving only the values that were emitted after this call, while old subscribers will still be receiving previously buffered values. To reset a shared flow to an initial value, emit the value after this call. more information
private val _reorder = MutableSharedFlow<ViewState<ReorderDto?>>().apply {
resetReplayCache()
}
val reorder: SharedFlow<ViewState<ReorderDto?>>
get() = _reorder
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | |
| Solution 2 | Adrian Mole |
