psteiger / flow-lifecycle-observer

A plugin for collecting Kotlin Flow in an Android Lifecycle-aware manner.
MIT License
57 stars 7 forks source link

Collection is cancelled and restarted on rotation #1

Open tom-pratt opened 3 years ago

tom-pratt commented 3 years ago

Usually ongoing operations should not be interrupted due to rotation. Suggest to add a delay before cancelling the job like the androidx livedata { } builder.

In their version they have an optional timeout parameter which means that observers in the newly created activity after rotation have time to subscribe before the observers in the old activity unsubscribe. Thereby keeping any upstream shared flow alive.

image

tom-pratt commented 3 years ago

Actually now that I go back and check i see that asLiveData() creates a live data which cancels in onInactive anway. So maybe simply using asLiveData is the best solution here.

psteiger commented 3 years ago

Actually now that I go back and check i see that asLiveData() creates a live data which cancels in onInactive anway. So maybe simply using asLiveData is the best solution here.

Let's talk a bit architecture here.

The problem with converting to LiveData in the ViewModel is that in that scenario, one needs to think twice when injecting repositories directly in use-cases implementations: you have now two sources of data: a LiveData, which is ViewModel-scoped, and a backing State/SharedFlow, implemented in the repositories/data sources, which might be Singleton-scoped. To circumvent that duality, you can make your use-cases ViewModel-scoped and inject the viewModels on them instead. But now, they are ViewModel-dependent and can not be singleton-scoped anymore. If well done, it probably introduces no bugs or inconsistencies, but it surely makes the architecture more complicated. In services, for example, you should not expect any LiveData usage or ViewModel access, so how would you access ViewModel-scoped objects?

One might think you can then convert to LiveData at the repositories and data sources. Then we go back to step 0: the repositories/data sources should not be platform-dependent. I implement mine as pure Kotlin modules. This enables a solid multi-platform core on your projects.

psteiger commented 3 years ago

Hi @tom-pratt ,

Revisiting this question, I see that your worries about a delay for keeping upstream flow alive can (and should) be easily handled by the SharedFlow itself -- the observer need not worry about that, nor should it.

You should set a delay in shareIn() using the stopTimeoutMillis param.

    • [stopTimeoutMillis] — configures a delay (in milliseconds) between the disappearance of the last
  • subscriber and the stopping of the sharing coroutine. It defaults to zero (stop immediately).

Example:

myFlow.shareIn(coroutineScope, SharingStarted.WhileSubscribed(stopTimeoutMillis=2000), replay=1)

Then, if all observers are gone (e.g. screen rotation), the flow will keep sharing for at most 2s (upstream flows will not be stopped).

chriscoomber commented 3 years ago

Weighing in my limited understanding, the Flow<T>.asLiveData function (or alternatively the liveData builder function) should be thought of as equivalent to the Flow<T>.shareIn function (or alternatively Flow<T>.stateIn).

In both cases you're doing the same thing: When the SharedFlow/LiveData becomes "active", it starts collecting the upstream flow (i.e. the flow you called the function on which might be a cold flow, or the contents the block you pass to liveData) and publishes it itself in a hot way. When the SharedFlow/LiveData becomes "inactive", wait for the timeout and then cancel the original flow and stop publishing.

The meaning of "active/inactive" means different things in different contexts. If it's a LiveData, then it's active if it has >0 active observers. If it's a SharedFlow, then similarly we would use SharingStarted,WhileSubscribed to only do the sharing when it has >0 subscribers.

So, @tom-pratt I'd recommend either using asLiveData (with timeout) and not using this library, or using shareIn (or stateIn) (with timeout) with SharingStarted.WhileSubscribed and using this library in your activities/fragments.

As @psteiger says, the advantage of the latter is that you can push shareIn all the way back to platform-independent code, though that timeout does feel like a platform-specific number, so I'm not totally sure.

psteiger commented 3 years ago

@chriscoomber ,

I agree with your comments.

I'd add that if you choose to use LiveData in your views, and you choose to implement timeout logic in ViewModel using asLiveData(), and you choose to use pure Flow on Data Source/Repository, you must know that your data source will only have the 'shared' behavior (1 upstream collection to N downstream collectors) if you only access your pure Flow data sources through your ViewModel's LiveData (as the Flow itself will really have only one collector: the LiveData).

Or you can use LiveData all the way from the Data Sources / Repositories down to the Views. Then, you create a dependency of the domain logic on the platform.

Or you can use SharedFlow on Data Sources/Repositories using shareIn(), and convert to LiveData on ViewModel setting the timeout. Then, you'll potentially have both timeouts (SharedFlow.WhileSubscribed) and LiveData.

There are some other possible combinations.

I prefer using Flow all the way from data sources to views setting SharedFlow.WhileSubscribed in shareIn().

This lib is for enabling the 'sharing' behavior for SharedFlow configured with SharedFlow.WhileSubscribed, where collectors are created on onStart or onResume and destroyed on onStop or onPause, mimicking the concept of 'active' of the LiveData component.

Choices :)