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

Hel*_*oCW 5 android kotlin kotlin-coroutines

代码A来自Flow的官方文章

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

我认为代码 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>
}
Run Code Online (Sandbox Code Playgroud)

代码B

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

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


//The same
Run Code Online (Sandbox Code Playgroud)

新增内容:

致 Tenfour04: 谢谢!

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

当我单击“开始”按钮显示信息(如果我使用Dispatchers.IO.

如果我使用Dispatchers.Main.

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

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

而且,在我flowOn(Dispatchers.IO)为流程添加后,代码 4 就可以了,即使它是在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")
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

代码 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
Run Code Online (Sandbox Code Playgroud)

代码 3 确定

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
Run Code Online (Sandbox Code Playgroud)

代码 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
Run Code Online (Sandbox Code Playgroud)

Ten*_*r04 5

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

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

viewModelScope.launch(Dispatchers.IO) {
Run Code Online (Sandbox Code Playgroud)

因为您通常不会在协程的顶层调用阻塞函数。如果是的话,您可以将这些碎片包裹在withContext(Dispatchers.IO) { }. 通常,将协程留在主调度程序上会更方便,因为 Android 中有很多非挂起函数需要从主线程调用它们。如果你翻转它,你可能需要withContext(Dispatchers.Main) { }比反转更多的地方,并且在协程实际启动之前还会产生一帧延迟。此外,如果您的协程与 ViewModel 中的属性进行交互,则如果您仅从主调度程序中接触这些属性,则可以避免并发访问这些属性时出现的潜在问题,因为它是单线程的。

可能存在例外情况,即您启动的协程不与任何此类 Main 所需的函数交互并且直接调用阻塞函数,但我认为这种情况应该很少见,特别是如果您练习良好的封装(请参阅此处)。如果将协程顶层的一段代码分解为自己的函数,则可以将该单独的函数放入一个挂起函数中,以便withContext(Dispatchers.IO)在必要时使用。那么你的顶级协程就会看起来非常干净。