使用Coroutines与Flow时,Repository和ViewModel的数据采集最佳实践
问题:Repository与ViewModel中Flow采集的最佳实践探讨
我正在开发一款缓存应用,遇到如下场景:DAO函数返回代表本地数据库数据流的Flow<List>,我在Repository中采集DAO函数的数据,又在ViewModel中采集Repository函数的数据。
想探讨这种在Repository和ViewModel两处采集数据的最佳实践:
- 该做法是否合理?
- 存在哪些潜在弊端或问题?
补充说明:本地数据库是应用的source of truth,核心数据处理均在此完成。由于DAO与Repository函数均返回Flow<List>,我认为需在Repository中采集DAO数据以监听数据库更新。
附上相关代码,恳请分享使用Coroutines和Flow时,Repository与ViewModel数据采集的最佳实践建议及经验。我不确定在ViewModel中再次采集数据是否合适,是否有更高效的方案,希望遵循最佳实践,优化Coroutines和Flow的使用。
DAO代码
@Dao interface MovieDao { @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun saveMovies(moviesEntity: MoviesEntity) @Query("SELECT * FROM movies") fun getMovies(): Flow<List<MoviesEntity>>? @Query("DELETE FROM movies") suspend fun clearMoviesListing() }
Repository接口
interface MovieRepository { fun getMoviesResult( category: String, fetchFromRemote: Boolean, ): Flow<Resource<List<Movies>>> }
Repository实现类
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 7) class MovieRepositoryImpl( private val movieApi: MovieApi, private val movieDao: MovieDao ) : MovieRepository { override fun getMoviesResult( category: String, fetchFromRemote: Boolean ): Flow<Resource<List<Movies>>> { return flow { while (true) { emit(Resource.Loading(true)) movieDao.getMovies()?.collect { localListing -> if (localListing.isNotEmpty() && !fetchFromRemote) { emit(Resource.Loading(false)) emit( Resource.Success(data = localListing.map { it.toMovies() }) ) } else { val remoteListing = try { movieApi.getMovies(category = category) } catch (e: IOException) { e.printStackTrace() emit(Resource.Error("Couldn't Reach Server, Check Your Internet Connection, Try Refresh")) null } catch (e: HttpException) { e.printStackTrace() emit(Resource.Error("Couldn't Reach Server, Check Your Internet Connection.")) null } if (remoteListing !== null) { emit(Resource.Loading(false)) remoteListing.let { movie -> movieDao.clearMoviesListing() movieDao.saveMovies(movie.toMoviesEntity()) movieDao.getMovies()!!.collect() { movieEntity -> emit( Resource.Success(data = movieEntity.map { it.toMovies() }) ) } } } emit(Resource.Error(message = "")) emit(Resource.Loading(false)) } } } } } }
ViewModel代码
private fun getMovieResult(category: String, fetchFromRemote: Boolean) { viewModelScope.launch { movieRepository.getMoviesResult(category, fetchFromRemote) .collect { results -> when (results) { is Resource.Success -> { results.data?.let { result -> state = state.copy(movies = result) } } is Resource.Error -> state = state.copy( error = results.message ?: "An unexpected error occurred" ) is Resource.Loading -> state = state.copy(isLoading = results.isLoading) } } } }
分析与最佳实践建议
1. 两处采集的合理性判断
ViewModel中采集Repository返回的Flow是完全合理的,符合分层架构的职责划分:
- Repository负责封装数据来源(本地DB+远程API),输出统一的业务数据流;
- ViewModel负责采集这个数据流,转换为UI状态分发给界面。
但你当前Repository中的实现存在严重的Flow使用错误,并非最佳实践。
2. 当前代码的潜在问题
(1)无限循环与嵌套collect导致资源泄漏
while(true)会让Flow永远不会结束,ViewModel采集后协程会持续运行,即使界面销毁也不会自动停止,引发资源泄漏;- 嵌套
collect(比如内部再次调用movieDao.getMovies()!!.collect)会产生多个Flow订阅,浪费资源的同时还可能引发重复UI更新。
(2)错误的数据流发射逻辑
- 远程请求成功后再次订阅DAO Flow,会导致每次DB更新都重复发射Success状态,而实际上只需要一次最新数据发射;
- 无意义的空Error发射(
emit(Resource.Error(message = "")))会干扰UI状态判断。
(3)不必要的非空断言风险
movieDao.getMovies()!!存在空指针崩溃风险,DAO的getMovies()应该返回非空Flow<List<MoviesEntity>>(空列表而非null)。
3. 优化后的最佳实践方案
(1)Repository层优化:用Flow操作符简化逻辑
Repository核心是暴露本地DB的Flow,按需触发远程同步,避免内部嵌套collect,改用操作符组合数据流:
@RequiresExtension(extension = Build.VERSION_CODES.S, version = 7) class MovieRepositoryImpl( private val movieApi: MovieApi, private val movieDao: MovieDao ) : MovieRepository { override fun getMoviesResult( category: String, fetchFromRemote: Boolean ): Flow<Resource<List<Movies>>> { // 先将本地DB的Flow转换为业务模型的Success状态流 val localMoviesFlow = movieDao.getMovies() .map { it.map(MoviesEntity::toMovies) } .map { Resource.Success(it) as Resource<List<Movies>> } return if (fetchFromRemote) { flow { emit(Resource.Loading(true)) // 执行远程同步 runCatching { movieApi.getMovies(category) } .onSuccess { remoteMovies -> movieDao.clearMoviesListing() movieDao.saveMovies(remoteMovies.toMoviesEntity()) } .onFailure { e -> val errorMsg = when (e) { is IOException -> "Couldn't Reach Server, Check Your Internet Connection, Try Refresh" is HttpException -> "Couldn't Reach Server, Check Your Internet Connection." else -> "An unexpected error occurred" } emit(Resource.Error(errorMsg)) } emit(Resource.Loading(false)) } // 合并加载状态流与本地数据流 .flatMapConcat { localMoviesFlow } } else { // 不请求远程,直接返回带加载状态的本地数据流 flow { emit(Resource.Loading(true)) emitAll(localMoviesFlow) emit(Resource.Loading(false)) } } } }
(2)ViewModel层优化:管理订阅生命周期
- 用
viewModelScope采集Flow是正确的,它会在ViewModel销毁时自动取消协程; - 多次调用时先取消之前的订阅,避免重复采集:
private var moviesJob: Job? = null private fun getMovieResult(category: String, fetchFromRemote: Boolean) { moviesJob?.cancel() moviesJob = viewModelScope.launch { movieRepository.getMoviesResult(category, fetchFromRemote) .collect { results -> when (results) { is Resource.Success -> { results.data?.let { result -> state = state.copy(movies = result) } } is Resource.Error -> { state = state.copy( error = results.message ?: "An unexpected error occurred" ) } is Resource.Loading -> { state = state.copy(isLoading = results.isLoading) } } } } }
(3)DAO层优化:返回非空Flow
修改getMovies()返回非空Flow,空数据时返回空列表而非null:
@Query("SELECT * FROM movies") fun getMovies(): Flow<List<MoviesEntity>> // 去掉可空标记
4. 核心原则总结
- Repository层:负责数据获取与同步,将本地DB Flow转换为业务模型Flow,用操作符组合数据流而非内部collect;
- ViewModel层:唯一负责采集Repository的Flow并转换为UI状态,利用
viewModelScope管理协程生命周期; - 禁止嵌套collect:这是Flow使用的常见误区,会导致逻辑混乱与资源泄漏;
- 本地DB作为唯一数据源:所有UI数据都来自本地DB的Flow,远程数据仅用于同步到DB,确保UI始终展示最新本地数据。
内容的提问来源于stack exchange,提问作者Tarek Hendi
相关产品推荐
相关产品推荐

