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

使用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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 01:12:36