Why sharedFlow collect won't execute this line? - android

I define a sharedFlow in MainViewModel like
class MainViewModel : ViewModel{
val _sharedFlow: MutableSharedFlow()
val sharedFlow = _sharedFlow.asSharedFlow()
}
Then I have a activity
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED){
viewModel.sharedFlow.collect{
Log.i("View","Here can be call.")
}
Log.i("View","this line will never call.")
}
}
when I click a button to emit , collect will be called , then when I rotate this or back to previous activity , when this activity destroy it should be leave"collect" block then execute
Log.i("View","this line will never call")
right? but it doesn't ,does anyone knows why ? thanks

SharedFlow is a hot stream of data, which is collected infinitely until subscriber is cancelled. This usually happens when the scope in which the coroutine is running is cancelled. When Activity is destroyed, the corresponding lifecycleScope and all launched coroutine are cancelled, item collection and emission are also cancelled. When coroutine is canceled it doesn't mean that the code after collect will be executed, but wise versa - the code after collect will not be executed due to cancellation of the coroutine.
So basically the line
Log.i("View","this line will never call")
will never be executed unless the collected Flow is cold(finite).
You can launch a separate coroutine to collect the flow, then the line, which prints the log, will be executed:
lifecycleScope.launch {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch {
viewModel.sharedFlow.collect{
Log.i("View","Here can be call.")
}
}
Log.i("View","this line will never call.")
}
}

Related

How this change from runBlocking to sharedFlow work?

I would like to ask you why does it work?
Normally when I used collectLatest with flow my data wasn't collected on time and the return value was empty. I have to use async-await coroutines, but I have read it blocks main thread, so it is not efficient. I've made my research and find the solution using sharedflow.
Previously:
suspend fun getList: List<Items> {
CoroutineScope(Dispatchers.Main).launch {
async {
flow.collectLatest {
myItems = it
}
}.await()
}
return myItems
}
or without await-async and it returns emptyList
now:
suspend fun getList: List<Items> {
val sharedFlow = flow.conflate().shareIn(
coroutineScopeIO,
replay = 1,
started = SharingStarted.WhileSubscribed()
)
return sharedFlow.first()
}
conflate means:
Conflates flow emissions via conflated channel and runs collector in a separate coroutine. The effect of this is that emitter is never suspended due to a slow collector, but collector always gets the most recent value emitted.
I'm not sure I understand it clearly. When I conflate flow, I just create seperate coroutine to emit what will be inside my another function as in my example shareIn().first() and using this variablesharedFlow which is suspended so will give the same effect I made asnyc-await, but in that case I do not block main thread, but only my exact *parentCoroutine-or-suspendFunction?
SharingStarted.WhileSubscribed()
It just means to start emit when subcribed.
conflate() has nothing to do with why this is working. The separate coroutine it talks about is run under the hood and you don't need to think about it. It's just to make sure your flow never causes the upstream emitter to have to wait for a slow collector, and your collector skips values if they are coming faster than it can handle them. conflate() makes it safe to have a slow collector without a buffer.
In your first code block, you are launching a new coroutine in a new CoroutineScope, so it is not a child coroutine and will not be waited for before the function returns. (Incidentally, this new coroutine will only finish when the Flow completes, and most types of Flows never complete.)
In the second code block, you are calling first() on the Flow, which suspends and gets the next value emitted by the flow and then returns that value without waiting for the Flow to complete.
Some other notes:
You should never use async { /*...*/ }.await() where await() is called immediately on the Deferred, because it is just a more convoluted version of withContext(/*...*/) { /*...*/ }.
It's a code smell to create a CoroutineScope that you never assign to a property, because the point of creating a scope is so you can manage the scope, and you obviously aren't managing it if you have no reference to it to work with.
You said you are worried about blocking the main thread, but nothing in the code you showed looks suspicious of blocking the main thread. But it's possible your flow that you are basing this on has blocking code in it. By convention it shouldn't. If that flow blocks, you should use the flowOn(Dispatchers.IO) operator on it at the source so downstream users don't have to worry about it.
Although your code worked, it doesn't make sense to create a SharedFlow in a function and immediately collect from it. It's not being shared with anything! Your code could be simplified to this equivalent code:
suspend fun getList: List<Items> {
return flow.first()
}

Question about launchWhenX and repeatOnLifecycle

I have the following code and a few questions that need to be answered:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
lifecycleScope.launchWhenResumed {
delay(2000)
Log.d("LifeCycleAware", "launchWhenStarted: before calling")
val result = differentDispatcher()
Log.d("LifeCycleAware", "launchWhenStarted: after calling $result")
}
}
private suspend fun differentDispatcher(): Int =
withContext(Dispatchers.Default) {
for (i in 1..5) {
delay(2000)
Log.d("LifeCycleAware", "Inside different Dispatcher")
}
return#withContext 9
}
override fun onStart() {
super.onStart()
Log.d("LifeCycleAware", "onStart")
}
override fun onStop() {
super.onStop()
Log.d("LifeCycleAware", "onStop")
}
As far as I understand, the method (or any of them) launchWhenResumed, is called when the Lifecycle has reached the RESUMED state, and I also know that when I move the app to the background, the corrutine will stop, but it will not stop if it has child corrutines running in another Dispatcher, so far so good.
So in this code, we determine that if I, in the middle of the loop that is in the differentDispatcher method, send the app to second, it will continue to run but when it finishes, the parent corrutine launched with launchWhenResumed, will not resume until it takes this RESUMED state again.
My first doubt is... if when the corrutine is finished running, I go to the background and return to the foreground, why is it not launched again, if I have returned to the RESUMED state?
I also know about the existence of the repeatOnLifecycle method, where, if I pass the Lifecycle.State.RESUMED state as parameter, it is executed every time, moreover, I know that in this case if I go to the background, the execution of the corrutine is completely suspended and when I go back to the foreground it starts from the beginning, but, why when I run with launchWhenResumed and the corrutine finishes it does not start again, but with repeatOnLifecycle it does? What does it do differently internally?
I guess the answer is because when I switch from background to foreground, the onCreate method is not called again, and I've checked that:
override fun onResume() {
super.onResume()
lifecycleScope.launchWhenResumed {
delay(2000)
Log.d("LifeCycleAware", "launchWhenStarted: before calling")
val result = differentDispatcher()
Log.d("LifeCycleAware", "launchWhenStarted: after calling $result")
}
}
This way it does re-launch because the onResume method does call again when I switch from background to foreground but then.... what kind of magic does the repeatOnLifecycle method do?
The key to understanding launchWhenResumed is to break it down into the two parts that is actually is: a launch and a whenResumed block. Looking at the source code, you'll see it is actually exactly that:
public fun launchWhenResumed(
block: suspend CoroutineScope.() -> Unit
): Job = launch { // It does a regular launch
lifecycle.whenResumed(block) // Then passes your block to whenResumed
}
A launch is a one time operation - a launch done in onCreate() will only run exactly once for each call to onCreate(). This is also why calling launch in onResume() will launch every time you hit onResume.
A call to launch finishes in one of two ways: all of the calls within that block complete normally or the CoroutineScope is cancelled. For lifecycleScope, that cancellation happens when the Lifecycle reaches DESTROYED.
So in a regular launch, work starts immediately, runs until everything completes (or the scope is cancelled), and that's it. It never runs again or restarts at all.
Instead, the whenResumed is an example of Suspend Lifecycle-aware coroutines:
Even though the CoroutineScope provides a proper way to cancel long-running operations automatically, you might have other cases where you want to suspend execution of a code block unless the Lifecycle is in a certain state.
Any coroutine run inside these blocks is suspended if the Lifecycle isn't at least in the minimal desired state.
So what whenResumed does is just pause the coroutine code when you fall below the resumed state, essentially meaning that if your Lifecycle stops being RESUMED, instead of your val result = differentDispatcher() actually resuming execution immediately and your result being delivered back to your code, that result is simply waiting for your Lifecycle to again reach RESUMED.
So whenResumed doesn't have any 'restarting' functionality - just like other coroutine code, it just runs the code you've given it and then completes normally.
You're right that repeatOnLifecycle is a very different pattern. As per the Restartable Lifecycle-aware coroutines, repeatOnLifecycle doesn't have any of the 'pausing' behavior at all:
Even though the lifecycleScope provides a proper way to cancel long-running operations automatically when the Lifecycle is DESTROYED, you might have other cases where you want to start the execution of a code block when the Lifecycle is in a certain state, and cancel when it is in another state.
So in the repeatOnLifecycle call, every time your Lifecycle reaches RESUMED (or what Lifecycle.State you want), the block is ran. When you fall below that state, the whole block is completely cancelled (very similar to when your LifecycleOwner reaches DESTROYED - that level of cancelling the whole coroutine scope).
You'll not the dire warning at the end of the page that talks about both of these APIs:
Warning: Prefer collecting flows using the repeatOnLifecycle API instead of collecting inside the launchWhenX APIs. As the latter APIs suspend the coroutine instead of cancelling it when the Lifecycle is STOPPED, upstream flows are kept active in the background, potentially emitting new items and wasting resources.
The fact that your differentDispatcher code continues to run in the background, despite being inside a whenResumed block is considered a bad thing - if that code was doing more expensive operations, like checking for the user's location (keeping GPS on), it would continue to use up system resources and the user's battery the whole time, even when you aren't RESUMED.

MutableStateFlow events being overwritten

In MyViewModel a MutableStateFlow is used to transmit events to the fragment.
When the value of the MutableStateFlow is changed the earlier values are being overwritten inside the coroutine. So never received by fragment.
internal class MyViewModel(application: Application) : AndroidViewModel(application) {
private val myMutableStateFlow = MutableStateFlow<MySealedClass>(MySealedClass.Dummy1())
private fun getData() {
viewModelScope.launch {
//yield()
myMutableStateFlow.value = MySealedClass.Dummy2()
myMutableStateFlow.value = MySealedClass.Dummy3()
}
}
}
internal class MyFragment : Fragment(){
private var uiStateJob: Job? = null
override fun onStart() {
super.onStart()
uiStateJob = lifecycleScope.launch {
myViewModel.getUiFlow().collect {
//do something
}
}
}
}
If yield() is commented Dummy2 event is never received by the Fragment. Dummy 3 is received though.
If yield() is uncommented Dummy2 & 3 are both received.
If the state values are changed outside the coroutine then both Dummy2 and Dummy3 are received.
I need to predictably receive all events in my fragment.
Is there a proper reasoning for this behaviour?
StateFlow is meant to represent a state. Each event is technically a new up-to-date state value, making the previous states obsolete. This type of flow is for situations when only the latest state matters, because its events are conflated. From the docs:
Updates to the value are always conflated. So a slow collector skips fast updates, but always collects the most recently emitted value.
Edit in response to your comment: yield() is a suspend function that forces the suspension of the current coroutine. Therefore it gives a chance to the other coroutine to progress until its next suspension point, this is why in that case the collect is "ready" before the first value is set (and emitted).
However you shouldn't rely on that because it's brittle: if the other coroutine gets modified and has extra suspension points by calling other suspend functions, it might not reach the collect call, and you would be back to the other behaviour.
If you consistently need all events, you have several options:
Switch to a cold flow, which will only start when you collect
Use a Channel (with or without buffer)
Use a SharedFlow and trigger the events start by using onSubscription

Cancelling custom CoroutineScope

val scope = CoroutineScope(
Job() + Dispatchers.Main
)
scope.launch {
beforeExecute()
val result = withContext(dispatcher) { doInBackground(*params) }
if (!isCancelled) {
postExecute(result)
} else {
cancelled(result)
}
status = Status.FINISHED
}
scope.cancel()
If i put scope.cancel() outside launch it cancels the coroutine immediately without calling launch block code.Is this expected?Why it happens?Should cancel be placed inside launch at end of launch block if i want coroutine to end once it finish executing code inside launch?
Update
As per Hau Luu's answer and Marko Topolnik's comment,
”at the end of launch, I think the task is done and you don't need to
manually cancel the Coroutine.”
and
“Once your task is done, the coroutine disappears from memory.”
But here in Case 2 ,if I start another launch it is executed unless we cancel the coroutine inside first launch as in Case 1.
So is there any surety that after task is completed the coroutine disappears from memory without us manually calling cancel() ?Bcoz compiler will never know which is the last launch that is going to execute after which it needs to end coroutine
Case 1
scope.launch {
Log.e("Task","1");
scope.cancel()
}
scope.launch {
Log.e("Task","2");
}
Only Task 1 is printed
Case 2
scope.launch {
Log.e("Task","1");
}
scope.launch {
Log.e("Task","2");
}
Both Task 1 and 2 are printed
Your code can be translated to natural language as "Cancel the given coroutine right after scope.launch is executed" so I think this is expected behavior.
And for the other question, we only want to cancel a coroutine when there is something wrong during the execution process - hey coroutine, during the execution of the task I gave you. if there is sth wrong happen. Kill yourself. So at the end of launch, I think the task is done and you don't need to manually cancel the Coroutine.
Update: I write this as an answer because I can't write code in comment.
CoroutineScope was designed to react to the lifecycle of the object that create/start/house a coroutine. So when you call the cancel method on a CoroutineScope, you're stoping everything. Stoping not canceling. All child coroutines that were created by the scope, all jobs they are executing, cancel them all, and nothing more. The job is done. That's is why you can't start another launch after scope.cancel
A CoroutineScope will create and hold refs to a bunch of Corrountine via builder methods like launch and async. When you want to cancel a specific Coroutine. You need to cancel the Job that returned by the builder. Not cancel the scope that is housing them.
https://kotlin.github.io/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html
val job1 = scope.launch{ print('Task 1') }
job1.cancel()
val job2 = scope.launch{ print('Task 2') }
Task 2 will be printed as normal.

withContext after a CoroutineScope.launch - will CoroutineScope.launch block?

I just stumbled over this code:
fun addHeaderAndSubmitList(list: List<SleepNight>?) {
adapterScope.launch {
val items = when (list) {
null -> listOf(DataItem.Header)
else -> listOf(DataItem.Header) + list.map { DataItem.SleepNightItem(it) }
}
// isn't there any code required to wait for the
// adapterScope.launch coroutine to finish?
withContext(Dispatchers.Main) {
submitList(items)
}
}
}
found in this file of the google sleeptracker example.
I already added my question as comment in the code example. I am new to coroutines but to my knowledge adapterScope.launch is non-blocking, so adapterScope.launch might not be finished until
withContext(Dispatchers.Main) {
submitList(items)
}
is reached? Am I wrong about this? If not, how to fix it?
See launch.
Launches a new coroutine without blocking the current thread [...]
Here's what happens:
addHeaderAndSubmitList uses launch to start some asynchronous work. The work will finish naturally or will be terminated when adapterScope's lifecycle ends. Meanwhile addHeaderAndSubmitList finishes immediately.
Whatever is inside launch {} runs sequentially. submitList(items) is called after val items = .... Each happens effectively on a different thread, but the order is guaranteed.
The code inside launch { } runs sequentially in a blocking fashion inside the adapterScope, meaning all the code above the withContext(Main) is run and finished before switching contexts to submit the list to the adapter on the main thread.
The entire code block is likely running in an Default or IO context, so it runs in a blocking fashion outside the main thread, until it reaches the withContext(Main) to post the results to the main thread.

Categories

Resources