I've read similar topics but couldn't find a proper answer:
In my Repository
class I have a cold Flow
that I want to share to 2 Presenters
/ViewModels
so my choice is to use shareIn
operator.
Let's take a look on Android docs' example:
val latestNews: Flow<List<ArticleHeadline>> = flow {
...
}.shareIn(
externalScope, // e.g. CoroutineScope(Dispatchers.IO)?
replay = 1,
started = SharingStarted.WhileSubscribed()
)
What docs suggests for externalScope
parameter:
A CoroutineScope that is used to share the flow. This scope should live longer than any consumer to keep the shared flow alive as long as needed.
However, looking for answer on how to stop subscribing a Flow
, the most voted answer in 2nd link says:
A solution is not to cancel the flow, but the scope it's launched in.
For me, these answers are contradictory in SharedFlow
's case. And unfortunately my Presenter
/ViewModel
still receives newest data even after its onCleared
was called.
How to prevent that? This is an example how I consume this Flow
in my Presenter
/ViewModel
:
fun doSomethingUseful(): Flow<OtherModel> {
return repository.latestNews.map(OtherModel)
If this might help, I'm using MVI architecture so doSomethingUseful
reacts to some intents created by the user.
Following the examples from Kotlin flows, a StateFlow can be exposed from the LatestNewsViewModel so that the View can listen for UI state updates and inherently make the screen state survive configuration changes. The class responsible for updating a MutableStateFlow is the producer, and all classes collecting from the StateFlow are the consumers.
Kotlin flows on Android 1 Creating a flow. To create flows, use the flow builder APIs. ... 2 Modifying the stream. ... 3 Collecting from a flow. ... 4 Catching unexpected exceptions. ... 5 Executing in a different CoroutineContext. ... 6 Flows in Jetpack libraries. ... 7 Convert callback-based APIs to flows. ... 8 Additional flow resources
You can turn cold flows hot by using the shareIn operator. Using the callbackFlow created in Kotlin flows as an example, instead of having each collector create a new flow, you can share the data retrieved from Firestore between collectors by using shareIn . You need to pass in the following: A CoroutineScope that is used to share the flow.
StateFlow and SharedFlow are Flow APIs that enable flows to optimally emit state updates and emit values to multiple consumers. StateFlow is a state-holder observable flow that emits the current and new state updates to its collectors. The current state value can also be read through its value property.
Thanks to Mark Keen's comments and post I think I managed to get a satisfactory result.
I've understand that scope defined in shareIn
parameter doesn't have to be a same scope that my consumer operates. Changing scope in BasePresenter
/BaseViewModel
from CoroutineScope
to viewModelScope
seems to solve the main problem. You don't even need to manually cancel this scope, as defined in Android docs:
init {
viewModelScope.launch {
// Coroutine that will be canceled when the ViewModel is cleared.
}
}
Just keep in mind that default viewModelScope
dispatcher is Main
which is not obvious and it might not be what you want! To change dispatcher, use viewModelScope.launch(YourDispatcher)
.
What is more, my hot SharedFlow
is transformed from another cold Flow
that is created on callbackFlow
callback API (which is based on Channels
API - this is complicated...)
After changing collection scope to viewModelScope
, I was getting ChildCancelledException: Child of the scoped flow was cancelled
exception when emitting new data from that API. This problem is well documented in both issues on GitHub:
As stated, there is a subtle difference between emission using offer
and send
:
offer is for non-suspending context, while send is for suspending ones.
offer is, unfortunately, non-symmetric to send in terms of propagated exceptions (CancellationException from send is usually ignored, while CancellationException from offer in nom-suspending context is not).
We hope to fix it in #974 either with offerOrClosed or changing offer semantics
As for Kotlin Coroutines of 1.4.2, #974 is not fixed yet - I hope it will in nearest future to avoid unexpected CancellationException
.
Lastly, I recommend to play with started
parameter in shareIn
operator. After all these changes, I had to change from WhileSubscribed()
to Lazily
in my use case.
I will update this post if I will find any new information. Hopefully my research would save someone's time.
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