psteiger / flow-lifecycle-observer

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

Support for `collectLatest` #6

Open chriscoomber opened 3 years ago

chriscoomber commented 3 years ago

I've found that most of the time in UI code you really want collectLatest rather than collect. If your UI code has gone away rendering a thumbnail or doing a bunch of heavy computations, you need to be able to interrupt that when the latest value of whatever flow you were watching is delivered (which may invalidate that work). Assuming the block of code that you pass to collectLatest employs cooperative cancellations (e.g. calling ensureActive() or yield() relatively often) then this all just works.

At the moment this library uses collect only.

I'm not exactly sure what I'd want the API to look like to support collectLatest. I ended up writing the following extension function instead of using your library. The following extension function simply launches and cancels, it's up to you what you want to put inside, whether it be collect, collectLatest, combine or whatever. This constitutes a solution for me, so this issue is by no means urgent, and I guess it's become more of a suggestion...

/**
 * Like [LifecycleCoroutineScope.launchWhenStarted], except instead of suspending the execution
 * of the block whilst not started, it is cancelled instead and relaunched.
 *
 * One use case for this is if the given block collects from a [SharedFlow] produced by [shareIn] or
 * [stateIn] using [SharingStarted.WhileSubscribed]. Suspending execution of the given block is not
 * sufficient to remove the block from the subscribers of the [SharedFlow], but cancelling it is. It
 * can be important to remove oneself from the subscribers to allow expensive upstream flows to stop
 * when the subscriber count reaches 0.
 *
 * For each purpose, this must be called at most once per lifecycle, since it re-launches the
 * given block of code again whenever the lifecycle gets into the required state. Calling this
 * from e.g. [Fragment.onStart] would cause job duplication after the fragment is stopped and
 * started a few times.
 *
 * If this is a view lifecycle ([Fragment.getViewLifecycleOwner]), then I suggest you run it in
 * [Fragment.onViewCreated]. If this is a regular lifecycle ([Fragment] or [Activity]), then I
 * suggest you run it in [Fragment.onCreate] or [Activity.onCreate].
 */
fun LifecycleOwner.relaunchWhenStarted(block: suspend CoroutineScope.() -> Unit) {
    lifecycle.addObserver(object : DefaultLifecycleObserver {
        private var job: Job? = null

        override fun onStart(owner: LifecycleOwner) {
            job = owner.lifecycleScope.launchWhenStarted(block)
        }

        override fun onStop(owner: LifecycleOwner) {
            job?.cancel()
            job = null
        }
    })
}
psteiger commented 3 years ago

@chriscoomber thanks a lot for your feedback. Indeed some more flexibility is welcome. Recently we introduced the possibility to use the onResume/onPause callbacks alternatively to onStart/onStop.

I will carefully consider your suggestions and think through how to introduce support for collectLatest in a way that is consistent with the current public API.