Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Kotlin Flow: unsubscribe from SharedFlow when Fragment becomes invisible

I've read similar topics but couldn't find a proper answer:

  • How to end / close a MutableSharedFlow?
  • Kotlin Flow: How to unsubscribe/stop
  • StateFlow and SharedFlow. Making cold flows hot using shareIn - Android docs
  • Introduce SharedFlow - GH discussion started by Roman Elizarov

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.

like image 863
adek111 Avatar asked Feb 18 '21 20:02

adek111


People also ask

What is a mutablestateflow in Kotlin?

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.

How to create a flow in Kotlin for Android?

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

How to turn cold flows hot in Kotlin?

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.

What is the difference between Stateflow and sharedflow?

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.


1 Answers

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:

  • kotlinx.coroutines.flow.internal.ChildCancelledException: Child of the scoped flow was cancelled
  • SendChannel.offer should never throw

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.

like image 84
adek111 Avatar answered Sep 22 '22 10:09

adek111