You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ViewModel中collect执行时随机出现Child of the scoped flow was cancelled错误排查

问题:调用Flow获取数据时随机出现“Child of the scoped flow was cancelled”错误

在ViewModel中每次输入查询词调用fetchGalleryItem Flow获取数据时,会随机抛出错误:Child of the scoped flow was cancelled。相关代码如下:

ViewModel代码

class PhotoGalleryViewModel:ViewModel() {
    private val photoRepository = PhotoRepository()
    private val preferencesRepository = PreferencesRepository.get()
    private val _uiState: MutableStateFlow<PhotoGalleryUiState> = MutableStateFlow(PhotoGalleryUiState())
    val uiState: StateFlow<PhotoGalleryUiState>
        get() = _uiState.asStateFlow()

    init {
        viewModelScope.launch() {
            preferencesRepository.storedQuery.collectLatest { storedQuery ->
                try {
                    fetchGalleryItems(storedQuery).cachedIn(viewModelScope).collect { fetchedItems ->
                        _uiState.update { oldState ->
                            oldState.copy(
                                images = fetchedItems,
                                query = storedQuery
                            )
                        }
                    }
                } catch (ex: Exception) {
                    Log.e(TAG, "Failed to fetch  items", ex)
                }
            }
        }
    }

    fun setQuery(query: String) {
           viewModelScope.launch {
               preferencesRepository.setStoredQuery(query)
           }
    }

    private fun fetchGalleryItems(query: String): Flow<PagingData<GalleryItem>> {
        return if (query.isEmpty()){
            photoRepository.fetchPhotos()
        } else{
            photoRepository.searchPhotos(query)
        }

    }
}

Fragment的onViewCreated代码

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    binding.photoGrid.adapter = adapter
    val menuHost: MenuHost = requireActivity()
    menuHost.addMenuProvider(object: MenuProvider{
        override fun onCreateMenu(menu: Menu, menuInflater: MenuInflater) {
            menuInflater.inflate(R.menu.fragment_photo_gallery, menu)
        }

        override fun onMenuItemSelected(menuItem: MenuItem): Boolean {
            return when (menuItem.itemId){
                R.id.menu_item_search -> {
                    searchView = menuItem.actionView as? SearchView
                    searchView?.setOnQueryTextListener(object: SearchView.OnQueryTextListener{
                        override fun onQueryTextSubmit(query: String?): Boolean {
                            photoGalleryViewModel.setQuery(query ?: "")
                            return true
                        }
                        override fun onQueryTextChange(newText: String?): Boolean {
                            return false
                        }
                    })
                    true
                }
                R.id.menu_item_clear -> {
                    photoGalleryViewModel.setQuery("")
                    true
                } else -> false
            }
        }
    },viewLifecycleOwner, Lifecycle.State.RESUMED)

    viewLifecycleOwner.lifecycleScope.launch {
        viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED){
            photoGalleryViewModel.uiState.collectLatest{
                adapter.submitData(it.images)
            }
        }
    }
}

问题根源

核心问题出在**collectLatest嵌套collect+cachedIn的错误使用方式**:

  1. preferencesRepository.storedQuery.collectLatest的特性是:新的查询词到来时,会立即取消上一次正在执行的协程(即上一轮的fetchGalleryItems(...).collect)。
  2. 你在每次收集fetchGalleryItems的Flow时都调用cachedIn(viewModelScope),这会将缓存Flow绑定到viewModelScope。当collectLatest取消上一轮协程时,缓存Flow的子协程会被连带取消,从而抛出该错误。
  3. 额外问题:PagingData设计初衷是直接配合collectLatest提交给Adapter,你通过StateFlow中转的方式冗余且容易触发不必要的数据刷新。

修复方案

方案1:优化Flow结构(推荐)

直接让ViewModel暴露PagingData的Flow,去掉中间的StateFlow中转,同时合理使用cachedIn:

class PhotoGalleryViewModel:ViewModel() {
    private val photoRepository = PhotoRepository()
    private val preferencesRepository = PreferencesRepository.get()
    
    // 直接暴露顶层Flow,cachedIn绑定到viewModelScope避免重复请求
    val galleryItems: Flow<PagingData<GalleryItem>> = preferencesRepository.storedQuery
        .flatMapLatest { storedQuery ->
            fetchGalleryItems(storedQuery)
        }
        .cachedIn(viewModelScope)

    fun setQuery(query: String) {
        viewModelScope.launch {
            preferencesRepository.setStoredQuery(query)
        }
    }

    private fun fetchGalleryItems(query: String): Flow<PagingData<GalleryItem>> {
        return if (query.isEmpty()){
            photoRepository.fetchPhotos()
        } else{
            photoRepository.searchPhotos(query)
        }
    }
}

对应Fragment修改收集逻辑:

viewLifecycleOwner.lifecycleScope.launch {
    viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED){
        photoGalleryViewModel.galleryItems.collectLatest {
            adapter.submitData(it)
        }
    }
}

方案2:保留StateFlow的兼容修改(不推荐)

如果坚持使用StateFlow中转,需要移除cachedIn,避免协程取消触发错误:

init {
    viewModelScope.launch {
        preferencesRepository.storedQuery.collectLatest { storedQuery ->
            // 移除cachedIn,避免子协程被取消时抛出异常
            fetchGalleryItems(storedQuery).collect { fetchedItems ->
                _uiState.update { oldState ->
                    oldState.copy(
                        images = fetchedItems,
                        query = storedQuery
                    )
                }
            }
        }
    }
}

关键说明

  • cachedIn应作用于顶层Flow,而非每次收集时调用,否则会创建多个缓存实例,增加协程取消引发错误的概率。
  • PagingData的Flow设计为直接与collectLatest+Adapter配合使用,中间通过StateFlow中转属于冗余操作,易引发状态更新混乱。

内容的提问来源于stack exchange,提问作者dev6573

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 10:53:10