'StateFlowImpl collect has a while loop,If I use it on UI Thread,Why it doesn't block UI Thread

If I use while loop on launch,it will keep running,the click event will not execute,eventually lead to ANR. StateFlowImpl collect has a while loop,When will it exit the loop,this is my case:

class MainActivity : AppCompatActivity(), CoroutineScope by MainScope() {
    private val TAG = "MainActivity"
    val flow = MutableStateFlow(0)
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        launch {
            while (true) {
                Log.d(TAG, "while")
            }
        }
        launch {
            flow.collect {
                Log.d(TAG, "onCreate: $it")
            }
        }
    }
}

// This is StateFlowImpl 
override suspend fun collect(collector: FlowCollector<T>) {
    val slot = allocateSlot()
    try {
        if (collector is SubscribedFlowCollector) collector.onSubscription()
        val collectorJob = currentCoroutineContext()[Job]
        var oldState: Any? = null // previously emitted T!! | NULL (null -- nothing emitted yet)
        while (true) {
            val newState = _state.value
            collectorJob?.ensureActive()
            if (oldState == null || oldState != newState) {
                collector.emit(NULL.unbox(newState))
                oldState = newState
            }
            if (!slot.takePending()) {
                slot.awaitPending()
            }
        }
    } finally {
        freeSlot(slot)
    }
}


Solution 1:[1]

"Blocking" and "never returning" are 2 different things.

The term "blocking" usually refers to using the thread exclusively, preventing it from doing other things (at least on the JVM).

Coroutines allow to have such a while(true) without blocking a thread. As long as there are suspension points in the loop, it gives an opportunity for the thread to go execute some other code from another coroutine and later come back.

  • In the case of StateFlowImpl, the collector.emit() call is a suspension point because emit() is a suspending function, so at that point the thread can go execute other coroutines.

  • If you don't have a suspension point (as in your first launch), the loop is indeed blocking the thread because it never yields it to other coroutines. This is what prevents other code from running on UI thread. You can artifically add suspension points in your loop by calling yield:

launch {
    while (true) {
        Log.d(TAG, "while")
        yield() // allow the thread to go execute other coroutines and come back
    }
}

You can also run blocking code on other threads than the main thread. This might be more appropriate if you're doing blocking IO or CPU-intensive stuff.

Note that using yield also makes this coroutine cancellable for free. Otherwise you would have to replace while(true) by while(currentCoroutineContext().isActive) to ensure you stop looping when the coroutine is cancelled.

When will it exit the loop

Now a while(true) loop indeed never returns. When you write the caller code, calling collect on StateFlow prevents any following code in the same coroutine from being executed. This is because code is executed sequentially within a coroutine even when suspend functions are involved (it makes it easy to reason about).

If you want to execute this collect concurrently with other code, you have to call it in a separate coroutine (using launch, async, or other coroutine builders) - and this is what you do here.

launch {
    flow.collect {
        Log.d(TAG, "onCreate: $it")
    }
    someOtherCode() // unreachable code
}
someOtherCode2() // this is OK

However, the coroutine calling StateFlow.collect never ends by itself, it needs to be cancelled from outside. This is usually controlled via the coroutine scope used to launch the coroutine.

In your case, you're making the activity implement CoroutineScope by MainScope(). This is not advisable, because you don't cancel that scope anywhere. Android already provides a ready-to-use coroutine scope in components such as Activities which have a lifecycle (see lifecycle-runtime-ktx library). It's called lifecycleScope. You should launch your coroutines in this scope so they automatically get cancelled when the activity is destroyed:

import androidx.lifecycle.lifecycleScope


lifecycleScope.launch { // cancelled when the Activity is destroyed
    while (true) {
        Log.d(TAG, "while")
        yield()
    }
}
lifecycleScope.launch { // cancelled when the Activity is destroyed
    flow.collect {
        Log.d(TAG, "onCreate: $it")
    }
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1