Collecting Kotlin Flows in a Lifecycle Aware Manner
Get rid of any abnormal flow behaviour by collecting it the right way.
First of all, What is Flow?
Kotlin Flows is now the most widely used way of handling streams of data. Kotlin Flows is built on top of coroutines which is Kotlin’s way of doing asynchronous reactive programming.
Kotlin Flow consists of three parts, the producer i.e. from where data is being sent or emitted, then comes the intermediary, which are operators to modify the data while being sent like mapping using map
, filtering using filter
, catching exceptions and many more. Then the last part is the consumer, the part which collects or observes the stream of data and reacts to it.
Problems with Collecting flow in UI Layer
As per the official Android documentation, we should be maintaining UI State using flows and continuously collect them in our Fragment/Activity and render the UI, but the issue arises that as Kotlin Flows are part of the Kotlin ecosystem which is not limited to Android but they are designed with a mindset of multi-platform capabilities hence it is not lifecycle-aware. The problem this causes is that flow does not know when the screen is visible and when not, it still keeps on emitting even though we don’t need it hence wasting resources.
Recent incident due to flow collection
We at Pratilipi Comics, use flows for managing UI State, and were using scope.launch()
to launch a coroutine to collect our flows. Recently we faced a crash in our HomeScreen, but the interesting thing was that this crash happened when our app was in the background. This info that the app crashed in the background hinted to us that Flows are still getting collected which they shouldn't be.
We got to know that our flows behaved like this because we were using scope.launch()
and we started researching this.
How we solved this issue?
We found out that there are APIs like scope.launchWhenStarted()
, scope.launchWhenCreated()
and scope.launchWhenResumed()
. These APIs suspend the coroutine i.e stop collection when views fall below the given state and then resume the coroutine i.e. resumes collection from where it left when it reaches the given state.
These APIs also have an issue which is that they only stop the collection but not the producer flow from emitting data, which means even though flows are not being collected but new data is still being emitted which eventually is wasting resources.
So what to use? How to collect Flows?
The recommended way to collect flows now is to use the repeatOnLifecycle()
API which takes a lifecycle State in its parameter. What this does is it would stop collection as well as emissions when LifecycleState goes below the given state by cancelling the coroutine and restarts when the given state is reached by launching a fresh new coroutine.
This is how we use repeatOnLifecycle()
Too much code repetition. How to clean it up?
Using this repeatOnLifecycle()
everywhere causes lots of code repetition. As per the DRY Principle, we should try to avoid this repetition.
What we can do is use the handy flowWithLifeCycle()
API provided to us by the Lifecycle framework. This function internally calls repeatOnLifecycle()
with the lifecycle state passed in the parameter.
Even after using flowWithLifecycle()
we have repetitions as we all going to collect all flows using this method. So why not utilize the power of Kotlin, create an extension function which wraps the collect()
and calls this flowWithLifecycle()
. By using such an extension functions our code is much cleaner and easier to modify if needed.
Things aren’t perfect and neither is repeatOnLifeycle()
After doing a bit of research we found out that, even though repeatOnLifecycle()
was the only solution to this problem, it wasn't perfect. It had a drawback that as it restarts the flow by creating a new coroutine, the flow pipeline is started again. For example, assume we have a flow which makes an API call, then use some intermediary operators to do some heavy calculations, preferably we only want to continue the collection and production of the flow, but what happens is that the whole flow pipeline is executed again.
This means that the API call is done again and all the intermediary operations are done again which is nothing but a waste of resources.
For example, look at this piece of code
val apiData = flow {
val result = api.call()
emit(result)
}
.mapLatest{
//doing some heavy operations
}
.onEach{
//doing some heavy operations
}
.... more intermediary operators
Now as we’re using repeatOnLifecycle()
every time our app goes in the background and is opened again, the flow would restart the whole chain, which is that it would make the API call again, execute all the intermediary operators, which is nothing but wasting resources as we didn't need to make API call again.
Solution to this Problem
Thanks to Hicham Boushaba’s blog we got a solution which is to make flows lifecycle aware without any interruption from the UI.
First of all, what we need to do is to create a flow which stores the current Lifecycle State of the UI.
Then we need to create an observer which listens to changes in the lifecycle of the UI and emits the state in our lifecycle flow.
Now the only thing left is to intercept all flows with this lifecycle flow using flatMapLatest
inside which we would check if the current Ui lifecycle state is below the given state, then an empty flow should be returned else the original flow. Something like
How this helps is that rather than creating a new coroutine which repeatOnLifeycle()
does, this just returns the Ui an empty flow while the screen is in the background else the original flow and hence the collection resumes again. So, there is no re-execution of the whole flow chain. To know more about how to code this up read this blog.
Conclusion
Managing Ui State using Flows is now the recommended way. Collecting flows can cause issues if not collected correctly. Flows not being lifecycle aware is what we need to take extra care of. Using just scope.launch()
can waste resources and can be dangerous too.
By using scope.launchWhenX()
APIs we can fix this, but this is not very perfect. Collecting flows should be done either using repeatOnLifecycle()
or flowWithLifecycle()
to avoid any kind of resource wastage and abnormal Ui behaviours.
Hope you liked this article. To read a few more of my articles head on to my Hashnode blogs If you want to connect with me, I’m available on these platforms: