Will collection data from Flow in viewModelScope maybe block UI in Android Studio?

Viewed 1203

The Code A is from official article about Flow

viewModelScope.launch{} run in UI thread by default, I think suspend fun fetchLatestNews() will run in UI thread by default too, so I think Code A maybe cause UI blocked when fetchLatestNews() is long time operation, right?

I think Code B can fix the problem, right?

Code A

class LatestNewsViewModel(
    private val newsRepository: NewsRepository
) : ViewModel() {

    init {
        viewModelScope.launch {
            // Trigger the flow and consume its elements using collect
            newsRepository.favoriteLatestNews.collect { favoriteNews ->
                // Update View with the latest favorite news
            }
        }
    }
}



class NewsRemoteDataSource(
    private val newsApi: NewsApi,
    private val refreshIntervalMs: Long = 5000
) {
    val latestNews: Flow<List<ArticleHeadline>> = flow {
        while(true) {
            val latestNews = newsApi.fetchLatestNews()
            emit(latestNews) // Emits the result of the request to the flow
            delay(refreshIntervalMs) // Suspends the coroutine for some time
        }
    }
}

// Interface that provides a way to make network requests with suspend functions
interface NewsApi {
    suspend fun fetchLatestNews(): List<ArticleHeadline>
}

Code B

class LatestNewsViewModel(
    private val newsRepository: NewsRepository
) : ViewModel() {

    init {
        viewModelScope.launch(Dispatchers.IO) {
           //The same
        }
    }
}


//The same

Added content:

To Tenfour04: Thanks!

I think a suspend function maybe block UI, so I think to define dispatchers is the most important in order not to block UI! Is it right?

Code 1 work well when I click "Start" button to display the information if I use Dispatchers.IO.

Code 2 is freezed and no information updated when I click "Start" button if I use Dispatchers.Main.

Code 3 can work just like Code 1 if I use Dispatchers.Main, the reason is only because I set delay(100).

BTW, Flow is suspend. So if there is a long time operation even if it's wrapped with coroutine instead of soundDbFlow().collect { myInfo.value = it.toString() }, I think I can get the same test result just like Code 1, Code 2 and Code 3.

And more, Code 4 is OK after I added flowOn(Dispatchers.IO) for the Flow even if it is launched in viewModelScope.launch(Dispatchers.Main){} !

Code 1 OK

class HandleMeter: ViewModel() {
    var myInfo = mutableStateOf("World")
    private var myJob: Job?=null
    private var k=0

    private fun soundDbFlow() = flow {
          while (true) {
             emit(k++)
             delay(0)
          }
   }

    fun calCurrentAsynNew() {
        myJob?.cancel()
        myJob = viewModelScope.launch(Dispatchers.IO){
            soundDbFlow().collect { myInfo.value = it.toString() }
        }
    }

    fun cancelJob(){
        myJob?.cancel()
    }

}


@Composable
fun Greeting(handleMeter: HandleMeter) {
    var info = handleMeter.myInfo
    Column(
        modifier = Modifier.fillMaxSize()
    ) {
        Text(text = "Hello ${info.value}")

        Button(
            onClick = { handleMeter.calCurrentAsynNew() }
        ) {
            Text("Start")
        }

        Button(
            onClick = { handleMeter.cancelJob() }
        ) {
            Text("Stop")
        }
    }
}

Code 2 Freezed

class HandleMeter: ViewModel() {
   private fun soundDbFlow() = flow {
          while (true) {
             emit(k++)
             delay(0)
          }
   }

    fun calCurrentAsynNew() {
        myJob?.cancel()
        myJob = viewModelScope.launch(Dispatchers.Main){
            soundDbFlow().collect { myInfo.value = it.toString() }
        }
    }
    ...
   //The same

}
...
//The same

Code 3 OK

class HandleMeter: ViewModel() {
   private fun soundDbFlow() = flow {
          while (true) {
             emit(k++)
             delay(100)  //It's 100
          }
   }

    fun calCurrentAsynNew() {
        myJob?.cancel()
        myJob = viewModelScope.launch(Dispatchers.Main){
            soundDbFlow().collect { myInfo.value = it.toString() }
        }
    }
    ...
    //The same

}
...
//The same

Code 4 Ok

class HandleMeter: ViewModel() {
   private fun soundDbFlow() = flow {
        while (true) {
            emit(k++)
            delay(0)
        }
    }.flowOn(Dispatchers.IO)// I added

    fun calCurrentAsynNew() {
        myJob?.cancel()
        myJob = viewModelScope.launch(Dispatchers.Main){
            soundDbFlow().collect { myInfo.value = it.toString() }
        }
    }

    ...
    //The same

}
...
//The same
3 Answers

A suspend function does not block unless it breaks the convention that a suspend function must never block. Therefore, it doesn't matter that your coroutine is called from the Main dispatcher. The main thread will not be blocked by calling fetchLatestNews(), unless you have improperly composed the implementation of the function such that it actually blocks.

You usually do not need to do this like in your Code B:

viewModelScope.launch(Dispatchers.IO) {

because you usually aren't calling blocking functions at the top level of your coroutine. If you are, you can wrap those pieces in withContext(Dispatchers.IO) { }. It's generally more convenient to leave the coroutine on the Main dispatcher, since there are so many non-suspend functions in Android that require that you call them from the main thread. If you flip it around you're likely to need withContext(Dispatchers.Main) { } in more places than you would have needed the inverse, and you also incur a one frame delay before the coroutine actually starts. Also, if your coroutines interact with properties in your ViewModel, you avoid potential problems with concurrent access to the properties if you only touch them from the Main dispatcher, since it is single-threaded.

There might be exceptions where you're launching a coroutine that doesn't interact with any such Main-required functions and that directly calls blocking functions, but I think that should be rare, especially if you practice good encapsulation (see here). If you break a chunk of code out of the top level of your coroutine into its own function, you can make that separate function into a suspend function that uses withContext(Dispatchers.IO) if necessary. Then your top level coroutine will look very clean.

The Code A will not block the UI thread, because the launch method does not block the current thread.

As the documentation says:

Launches a new coroutine without blocking the current thread and returns a reference to the coroutine as a [Job].

If the context does not have any dispatcher nor any other [ContinuationInterceptor], then [Dispatchers.Default] is used.

So in your case, CodeA uses the Dispatches.Default under the hood, while CodeB uses the Dispatchers.IO

More on coroutines here

@qki wrote right. But his answer has inaccuracy. ViewModel have viewModelScope. viewModelScope have context SuperVisorJob() + Dispatchers.Main.immediatly. LatestNewsViewModel's init will be execute in main group threads, but this will not block ui thread.

Related