
Github Actions
GitHub Actions for Android CI/CD
Published on Oct 14, 2022 by Man Ho on Android Development
This is the original article of Manuel Vivo from Medium: https://medium.com/androiddevelopers/a-safer-way-to-collect-flows-from-android-uis-23080b1f8bda
He mentioned Lifecycle.repeatOnLifecycle
and Flow.flowWithLifecycle
, and it should be used to replace old collect flow ways like CoroutineScope.launch
, Flow.launchIn
or even LifeCycleCoroutineScope.launchWhenX
.
As we all know, Flow is not like LiveData, it does not automatically stop collecting data when the app is in the background. Until the appearance of LifeCycleCoroutineScope.launchWhenX
, the problem is somewhat solved, but it’s still not perfect, even though the collect is suspended while the app is in the background, the flow producer still active and continues to emit the data even though no one is collecting. Even using lifecycleScope.launch
or launchIn
is dangerous than when it continues to collect data even though the app is in the background.
You may have thought about manually canceling the coroutine when the app goes into the background, it would work fine, but in reality it will create quite a lot of boilerplate code, you will have to write the same thing in many different places.
And that’s why Lifecycle.repeatOnLifecycle
and flowWithLifecycle
were introduced from androidx.lifecycle:lifecycle-runtime-ktx:2.4.0
library or later.
For better understanding, read through the article by Manuel Vivo , and here we will try above things to see what is the difference. ;)
Now we will start dissecting the behavior of each collector!
lifecycleScope
:onStop
, it still collects value, so at the end we can see it shows all value from 0 to 10.launchWhenCreated
:onCreate
and before onStart
), so it can’t collect value 0, instead the first value is 1.lifecycleScope
, it still collects value, and at the end we can see from 1 to 10.launchWhenStarted
:onStart
and before onResume
), so it also can’t collect value 0, and 1 is the first value.onPause
is called.launchWhenResumed
:launchWhenStarted
, but the difference is it starts collect after onResume
is called.launchWhenStarted
, it will be suspended before onPause
is called.repeatOnLifecycle - CREATED
:launchWhenCreated
, it starts collecting right after onCreate
is called.repeatOnLifecycle - STARTED
:launchWhenStarted
, it starts collecting right after onStart
is called.repeatOnLifecycle - RESUMED
:launchWhenResumed
, it starts collecting right after onResume
is called and only in resume state.With flowWithLifecycle - CREATED
: The same result as launchWhenCreated
and repeatOnLifecycle - CREATED
With flowWithLifecycle - STARTED
: The same result as launchWhenStarted
and repeatOnLifecycle - STARTED
flowWithLifecycle - RESUMED
:The same result as launchWhenResumed
and repeatOnLifecycle - RESUMED
After above tests, we can separate it to 4 types:
lifecycleScope.launch
-> start collecting right after creating and only canceling when the lifecycle owner is destroyed.launchWhenCreated
, repeatOnLifecycle.CREATED
, flowWithLifeCycle.CREATED
have the same result on above test.launchWhenCreated
, repeatOnLifecycle.STARTED
, flowWithLifeCycle.STARTED
have the same result on above test.launchWhenCreated
, repeatOnLifecycle.RESUMED
, flowWithLifeCycle.RESUMED
have the same result on above test.So we will not talk about lifeCycleScope.launch
here, instead we will clarify why we have 3 options with the same result, and if they are really the same, let’s go to the second test!
Take a look at my stateFlow: SecondViewModel
Take a look at my activity: SecondActivity
As I said at the First test, on the surface, it’s clear that all three methods give the same result, but is it really the same in practice? To put it mildly, no one would spare to create 3 different things for the same result, right? As I mentioned, repeatOnLifeCycle
was created to replace launchWhenX
, because in fact launchWhenX
will still create a waste of resources when the lifecycle owner is inactive, and flowWithLifecycle
is also not the same as repeatOnLifeCycle
, it is an operator. And the main difference is:
launchWhenX
: it suspends only when the lifecycle is out of target state, and it simply suspends and the coroutine remains active, it will continue when the lifecycle is in target state.repeatOnLifeCycle
: it doesn’t suspend, but cancels the coroutine when the lifecycle goes out of target state. That means, each time the lifecycle comes back into target state, it creates a new coroutine and runs it.flowWithLifecycle
: it’s an operator, and the order in which the operator is placed will also affect the collector’s behavior, but we’re not going to talk about operators for the time being in this article.For better understanding, see the execution result:
SecondTest.onStop D is job1 active: 109145133 - true
SecondTest.onStop D is job1 canceled: 109145133 - false
SecondTest.onStop D is job2 active: 199161186 - false
SecondTest.onStop D is job2 canceled: 199161186 - true
SecondTest.onStop D is job3 active: 142509811 - true
SecondTest.onStop D is job3 canceled: 142509811 - false
SecondTest.onStop D is job1 active: 109145133 - true
SecondTest.onStop D is job1 canceled: 109145133 - false
SecondTest.onStop D is job2 active: 266476678 - false
SecondTest.onStop D is job2 canceled: 266476678 - true
SecondTest.onStop D is job3 active: 142509811 - true
SecondTest.onStop D is job3 canceled: 142509811 - false
SecondTest.onStop D is job1 active: 109145133 - true
SecondTest.onStop D is job1 canceled: 109145133 - false
SecondTest.onStop D is job2 active: 51376120 - false
SecondTest.onStop D is job2 canceled: 51376120 - true
SecondTest.onStop D is job3 active: 142509811 - true
SecondTest.onStop D is job3 canceled: 142509811 - false
SecondTest.onDestroy D is job1 active: 109145133 - false
SecondTest.onDestroy D is job1 canceled: 109145133 - true
SecondTest.onDestroy D is job2 active: 51376120 - false
SecondTest.onDestroy D is job2 canceled: 51376120 - true
SecondTest.onDestroy D is job3 active: 142509811 - false
SecondTest.onDestroy D is job3 canceled: 142509811 - false
* The results show that:
* _job1_ and _job3_ only **cancel** and **stay active** when `onStop` is called
* _job2_ is **canceled** and **no longer active** when `onStop` is called
* _job1_ will be **canceled** when `onDestroy` is called
* _job3_ just **no longer active** but **still not canceled** when `onDestroy` is called
* _job1_ and _job3_ are always **1 instance** even after 2 times `onPause` is called
* _job2_ always **creates a new instance** after every `onStart` is called (
repeatOnLifeCycle - STARTED)
* This it can be deduced that:
* `launchWhenX` and `flowWithLifecycle` will not automatically cancel when lifecycle goes
out of target state.
* `repeatOnLifecycle` always cancels and starts a new coroutine.
* And as one has already mentioned:
* `launchWhenX` will be deprecated in the future, and actually we should use
`repeatOnLifeCycle`.
* `flowWithLifecycle` was introduced with `repeatOnLifeCycle`, it is not the default way
when we start collecting data, but it is an operator, it will be used in a sequence of
operators to perform the collection. It can be used in some cases, and ordering the
operators will also affect the collector's behavior. So let's take a third test
with `flowWithLifecycle` to see how it works!
flowWithLifecycle
is an operator, so the ordering of operator will affect to the collector.repeatOnLifeCycle
to wrap all of them inside.So let’s try this operator in practice!
And I try two collector here: ThirdActivity
- The first collector (1): I add `combine` before and then `flowWithLifecycle`
- The second collector (2): I add `flowWithLifecycle` before and then `combine`
- since `combine` will create another flow, so in case of **first collector**
, `flowWithLifecycle`
will be applied to `combine`, so it don't collect the flow when activity is in background.
- In the other hand, **collector 2** still collect the flow when activity is in background.
- The third collector (3): I add `map` before and then `flowWithLifecycle`
- The fourth collector (4): I add `flowWithLifecycle` before and then `map`
- since `map` don't create another flow, it's just a transformer, so the collector only
collects data in foreground (for both of (3) and case (4))
In normal case, we can safety use repeatOnLifecycle
, we will no longer care about wasting resources, the only thing we need to do is choose the right lifecycle state to start collecting.
launchWhenX
will be deprecated in the future, so please migrate to using repeatOnLifecycle
.
flowWithLifecycle
is an operator, operator has its cool stuff, and using chain of operators can get quite complicated if you don’t really understand what each operator does.
In short, try them all and find the best solution for your work ;)
Happy coding!
GitHub Actions for Android CI/CD
Channels & operators
StateFlow & SharedFlow