4. Repository模式封装数据层:单一数据源原则、DAO注入、数据操作封装、LiveData/Flow暴露
在WorkManager与Room后台同步的架构中,数据层的核心职责是统一管理本地数据库与远程服务器的数据流,并向上层(ViewModel、WorkManager)提供干净、可测试的接口。Repository模式正是实现这一目标的最佳实践。本章将系统讲解单一数据源原则、DAO注入、数据操作封装以及通过LiveData / Flow暴露数据的具体实现。
4.1 单一数据源原则
单一数据源(Single Source of Truth,SSOT)意味着应用中的所有数据变更只能通过一个受控的数据层入口,避免分散的读写路径导致数据不一致。在Room + WorkManager的异步同步场景下,本地数据库始终作为唯一可信数据源。
- 本地权威:UI层仅从Room数据库读取数据,即使远程数据存在,也必须先写入Room再通知UI。
- 同步透明:WorkManager执行同步任务时,只操作Repository提供的API,不直接访问DAO或网络。
- 数据流单向:网络数据 → Repository → Room → LiveData/Flow → UI。
| 角色 | 是否可写 | 数据流向 |
|---|---|---|
| Room DAO | 仅由Repository内部写 | UI通过Repository暴露的LiveData/Flow读取 |
| 网络API | 仅由Repository内部调用 | 数据降级到Room后由Repository传递 |
| ViewModel | 不可直接写DAO | 通过Repository封装的方法触发写操作 |
| WorkManager | 不可直接写DAO | 通过Repository的同步方法写入 |
避坑提示: 不要直接从WorkManager或ViewModel中调用DAO的insert/update方法,否则会破坏单一数据源,导致页面直接对数据库的缓存与Repository的同步逻辑产生冲突(如重复写入、状态不同步)。
4.2 DAO注入与依赖解耦
Repository需要对Room DAO和网络API进行构造函数注入,通常使用Hilt/Dagger或手动依赖工厂。注入的目的:
- 测试替代:在单元测试中可以轻松传入Mock DAO和Mock ApiService。
- 生命周期管理:Repository跟随Application作用域,DAO实例由Room数据库提供,不持有Activity引用。
- 关注点分离:Repository不负责创建DAO,仅依赖接口。
class SyncRepository @Inject constructor(
private val syncDao: SyncDao,
private val syncApiService: SyncApiService
) {
// 数据操作封装...
}
常见错误: 在Repository中直接使用 new SyncDao() 或手动调用 Room.databaseBuilder(...) 创建DA实例。这会使得Repository与Room具体实现强耦合,无法在测试中替换,且容易因多次实例化导致数据库连接泄漏。务必通过依赖注入或 Application 级单例传递DAO。
4.3 数据操作封装:同步与异步方法
Repository暴露的方法分为两类:纯查询(Room挂起/Flow)和写入/同步(挂起函数或WorkManager驱动)。封装时必须考虑以下规则:
- 返回类型统一:查询操作返回Flow<T>或LiveData<T>;写入操作返回Result<Unit>或挂起方法。
- 异常处理:网络请求、数据库写入错误应在Repository内部捕获并返回失败状态,而非向上抛出原始异常。
- 幂等性:多次触发相同同步请求不应产生重复数据,主要通过唯一ID(如服务器ID)保证。
class SyncRepository @Inject constructor(
private val syncDao: SyncDao,
private val syncApiService: SyncApiService
) {
// 查询 - 返回 Flow
fun getSyncItems(): Flow<List<SyncItem>> = syncDao.getItemsFlow()
// 插入/更新 - 挂起函数
suspend fun insertItem(item: SyncItem): Result<Unit> = try {
syncDao.insert(item)
Result.success(Unit)
} catch (e: Exception) {
Result.failure(e)
}
// 远程同步: 从服务器拉取并替换本地缓存
suspend fun refreshFromRemote(): Result<Int> = try {
val remoteItems = syncApiService.fetchItems()
syncDao.deleteAll()
syncDao.insertAll(remoteItems)
Result.success(remoteItems.size)
} catch (e: Exception) {
Result.failure(e)
}
}
优化建议: 对于批量写入操作,使用Room的 @Transaction 注解保证原子性,同时避免在UI线程执行挂起函数。WorkManager的Worker内部调用Repository的挂起函数时,使用 withContext(Dispatchers.IO) 确保IO调度。
4.4 LiveData / Flow暴露:让数据成为可观察对象
Repository的查询方法应当返回Reactive类型,让数据层自动通知上层变化。Kotlin首选Flow,Java / 旧项目可以使用LiveData。
- Flow优点:支持背压、冷流/热流切换、与协程天然集成;Room DAO直接支持返回Flow。
- LiveData优点:生命周期感知,适合Android传统UI绑定(但Room的LiveData在后台线程同步时可能存在粘性事件问题)。
推荐在Repository层统一返回Flow,然后在ViewModel中通过stateIn或asLiveData()转换给UI。
// Repository 返回 Flow
fun getSyncItemsFlow(): Flow<List<SyncItem>> = syncDao.getItemsFlow()
// ViewModel 中使用
class SyncViewModel @Inject constructor(
private val repository: SyncRepository
) : ViewModel() {
val items: StateFlow<List<SyncItem>> = repository.getSyncItemsFlow()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
}
实战场合: 若Worker需要监听同步结果并更新进度,可在Repository中暴露一个
SharedFlow<SyncProgress> ,在Worker内部收集并报告给WorkManager的SettleProgress API。这样UI和Worker都可以订阅同一数据流,保持状态一致。
4.5 数据层整体结构图
下图展示了Repository在单一数据源模式下的完整数据流与各组件间的依赖关系:
4.6 调试与测试策略
Repository层是测试的核心单元。以下是常用调试与验证手段:
- 单元测试:使用Mockito或MockK模拟DAO和ApiService,验证Repository方法是否按预期调用。
- 数据库检测:打开Room的SQLite日志(
databaseBuilder.setQueryCallback())查看实际SQL。 - 端到端验证:在Worker中注入真实的Repository,但使用内存数据库(
.inMemoryDatabaseBuilder())避免污染生产数据。
性能优化: Repository层不需要缓存额外数据——Room的Flow已经是反应式缓存。避免在Repository内再创建HashMap或MutableStateFlow进行“手动缓存”,否则会产生多份数据副本,增加同步复杂度且可能造成内存泄漏。