在 viewModelScope 中从 Flow 收集数据是否会在 Android Studio 中阻塞 UI?

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

代码A来自官方article关于Flow

viewModelScope.launch{} 运行 默认在 UI 线程中,我认为 suspend fun fetchLatestNews() 默认也会 运行 在 UI 线程中,所以我认为代码 A 可能导致 UI 在 fetchLatestNews() 长时间操作时被阻塞,对吗?

我认为代码 B 可以解决问题,对吗?

代码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>
}

代码B

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

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


//The same

新增内容:

致 Tenfour04:谢谢!

我认为挂起函数可能会阻塞UI,所以我认为定义dispatchers是最重要的,以免阻塞UI!对吗?

如果我使用 Dispatchers.IO.

,当我单击“开始”按钮显示信息时,代码 1 工作正常

如果我使用 Dispatchers.Main.

,当我单击“开始”按钮时,代码 2 被冻结并且没有更新任何信息

如果我使用Dispatchers.Main,代码3可以像代码1一样工作,原因只是因为我设置了delay(100)

顺便说一句,Flow 已暂停。所以如果有一个长时间的操作,即使它用协程包裹而不是soundDbFlow().collect { myInfo.value = it.toString() },我想我可以得到与代码1、代码2和代码3相同的测试结果。

此外,代码 4 在我为 Flow 添加 flowOn(Dispatchers.IO) 后就可以了,即使它是在 viewModelScope.launch(Dispatchers.Main){} 中启动的!

代码 1 正常

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")
        }
    }
}

代码 2 冻结

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

代码 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

代码 4 正常

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

代码A不会阻塞UI线程,因为launch方法不会阻塞当前线程

如文档所述:

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.

因此,在您的情况下,CodeA 在后台使用 Dispatches.Default,而 CodeB 使用 Dispatchers.IO

关于协程的更多信息here

@qki 写对了。但他的回答并不准确。 ViewModel 有 viewModelScope。 viewModelScope 有上下文 SuperVisorJob() + Dispatchers.Main.immediatly。 LatestNewsViewModel 的 init 将在主组线程中执行,但这不会阻塞 ui 线程。

挂起函数不会阻塞,除非它打破了挂起函数绝不能阻塞的约定。因此,从主调度程序调用协程并不重要。调用 fetchLatestNews() 不会阻塞主线程,除非 您不正确地编写了函数的实现,以至于它实际上阻塞了。

您通常不需要像在您的 代码 B 中那样执行此操作:

viewModelScope.launch(Dispatchers.IO) {

因为您通常不会在协程的顶层调用阻塞函数。如果是,您可以将这些部分包装在 withContext(Dispatchers.IO) { } 中。通常将协程留在主调度程序上更方便,因为 Android 中有很多 non-suspend 函数需要您从主线程调用它们。如果你翻转它,你可能需要 withContext(Dispatchers.Main) { } 在更多的地方比你需要相反的地方,并且你也会在协程实际启动之前产生一帧延迟。此外,如果您的协程与 ViewModel 中的属性交互,如果您只从 Main 调度程序接触它们,则可以避免并发访问属性的潜在问题,因为它是 single-threaded.

当您启动一个不与任何此类 Main-required 函数交互并直接调用阻塞函数的协程时,可能会有例外情况,但我认为这种情况应该很少见,尤其是如果您练习良好的话封装(see here)。如果将协程顶层的一大块代码分解成它自己的函数,则可以将该单独的函数变成一个挂起函数,必要时使用 withContext(Dispatchers.IO)。那么你的顶层协程看起来会很干净。