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.
In the example below, you’d think that both Flow s are being collected at the same time, but flow1 is collected until the Flow finishes emitting, and then flow2 is collected until it is finished emitting.
This is the equivalent logic as above, but using launchIn. This is less code to write, but more importantly it’ll get you out of some hard to debug situations when collecting from Flow s. The non obvious thing to understand is that collect () will block the coroutine until the flow has finished emitting.
The practical difference then is, that you can call collect () method only from another suspending function or from a coroutine. For example like this: whereas .launchIn () can be called like this in any regular function: With the .launchIn () method, you also get a Job as return value, so you could cancel the flow by cancelling the job:
It runs great for me in the dev environment. It runs twice, because an update of Opportunities in the flow triggers it to run again. The field that was updated in the first run is examined and the flow is aborted on that second run. The problem is, something is triggering the flow multiple times in the test instance.
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.
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)
}
}
In Activity you can simply collect data in onCreate.
lifecycleScope.launchWhenStarted {
availableLanguagesFlow.collect {
languagesAdapter.setItems(it.allItems, it.selectedItem)
}
}
extension:
fun <T> Flow<T>.launchWhenStarted(lifecycleOwner: LifecycleOwner) {
lifecycleOwner.lifecycleScope.launchWhenStarted {
[email protected]()
}
}
in fragment onViewCreated:
availableLanguagesFlow
.onEach {
//update view
}.launchWhenStarted(viewLifecycleOwner)
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)
}
}
}
}
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With