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 {
this@launchWhenStarted.collect()
}
}
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 {
this@launchWhenStarted.collect()
}catch (t: Throwable){
loge(t)
}
}
}
}