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的错误使用方式**:
preferencesRepository.storedQuery.collectLatest的特性是:新的查询词到来时,会立即取消上一次正在执行的协程(即上一轮的fetchGalleryItems(...).collect)。- 你在每次收集
fetchGalleryItems的Flow时都调用cachedIn(viewModelScope),这会将缓存Flow绑定到viewModelScope。当collectLatest取消上一轮协程时,缓存Flow的子协程会被连带取消,从而抛出该错误。 - 额外问题:
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
相关产品推荐
相关产品推荐

