19、Repository模式:Repository设计、数据源抽象、缓存策略、单一数据源
Repository模式,说白了就是给数据访问加个「中间人」。
我刚开始做Android开发那会儿,代码里到处都是直接调SQLite、直接发网络请求。后来项目大了,想换个数据源,好家伙,改代码改到怀疑人生。那时候我就意识到——数据访问必须统一收口。
Repository就是干这个的。它把数据从哪里来、怎么缓存、怎么同步这些破事,全封装在背后。上层ViewModel只管说「我要数据」,Repository说「好,我给你」。至于数据是从网络拿、从数据库读、还是从缓存取,ViewModel一概不知。
19.1 Repository的核心设计思想
Repository的本质,是一个数据访问的抽象层。它不关心数据的具体来源,只关心对外提供干净、一致的数据接口。
我个人习惯把Repository设计成接口+实现的结构。接口定义「做什么」,实现定义「怎么做」。这样测试的时候,我可以轻松替换成Mock数据源。
核心原则:
- ViewModel只依赖Repository接口,不依赖具体实现
- Repository内部组合多个数据源(Local、Remote、Cache)
- Repository对外暴露Flow或suspend函数,保持异步友好
你想想看,如果哪天老板说「把本地数据库从Room换成SQLDelight」,你只需要改Repository内部的实现,ViewModel一行代码都不用动。这就是抽象的价值。
19.2 数据源抽象:Local与Remote的分工
数据源抽象,说白了就是把「本地数据」和「远程数据」拆成两个独立的模块。它们各自只干一件事。
我一般这样划分:
| 数据源类型 | 职责 | 典型实现 |
|---|---|---|
| LocalDataSource | 持久化存储、离线缓存 | Room DAO、SharedPreferences |
| RemoteDataSource | 网络请求、实时数据 | Retrofit、Firebase |
| CacheDataSource | 内存缓存、临时快照 | HashMap、LRU Cache |
举个例子,假设我们要做一个用户信息Repository:
interface UserRepository {
fun getUser(userId: String): Flow<User>
suspend fun refreshUser(userId: String)
}
class UserRepositoryImpl(
private val localDataSource: UserLocalDataSource,
private val remoteDataSource: UserRemoteDataSource
) : UserRepository {
override fun getUser(userId: String): Flow<User> {
// 先读本地,再尝试刷新
return localDataSource.observeUser(userId)
.onStart {
val cached = localDataSource.getUser(userId)
if (cached == null) {
refreshUser(userId)
}
}
}
override suspend fun refreshUser(userId: String) {
try {
val remoteUser = remoteDataSource.fetchUser(userId)
localDataSource.saveUser(remoteUser)
} catch (e: Exception) {
// 网络失败,静默处理
// 用户仍然能看到本地缓存数据
}
}
}
这里有个细节:getUser返回的是Flow。这意味着只要本地数据变了,ViewModel那边会自动收到更新。嗯,这就是响应式的魅力。
我的经验:不要把网络请求的结果直接返回给ViewModel。先存到本地,再从本地读出来。这样数据流是单向的、可追溯的。我曾经在一个项目里直接返回网络数据,结果网络失败时UI直接崩了——从那以后我再也不这么干了。
19.3 缓存策略:怎么让数据又快又新鲜
缓存策略,说白了就是「什么时候读本地,什么时候读网络」。我常用的策略有三种:
- 网络优先:每次先请求网络,成功就更新本地,失败就读本地缓存。适合实时性要求高的场景,比如股票行情。
- 缓存优先:先读本地,有数据就直接返回,后台静默刷新。适合用户资料、配置信息这类不常变的数据。
- 过期刷新:本地数据带一个时间戳,如果没过期就直接用,过期了才去网络拉。适合新闻列表、商品列表。
我个人最常用的是「缓存优先+后台刷新」。用户体验最好——打开App瞬间看到数据,几秒后自动更新成最新内容。
class NewsRepositoryImpl(
private val local: NewsLocalDataSource,
private val remote: NewsRemoteDataSource
) : NewsRepository {
companion object {
private const val CACHE_EXPIRY = 5 * 60 * 1000L // 5分钟
}
override fun getNews(): Flow<List<News>> {
return local.observeAllNews()
.onStart {
val lastFetch = local.getLastFetchTime()
if (lastFetch == null ||
System.currentTimeMillis() - lastFetch > CACHE_EXPIRY) {
refreshNews()
}
}
}
private suspend fun refreshNews() {
try {
val news = remote.fetchNews()
local.replaceAll(news)
local.setLastFetchTime(System.currentTimeMillis())
} catch (e: Exception) {
// 静默失败,用户看到的是缓存数据
}
}
}
注意:缓存过期时间不要写死。我建议把它做成可配置的,甚至可以从后端下发。我曾经在一个新闻App里把缓存设成30分钟,结果用户骂「新闻都变旧闻了」——嗯,后来改成5分钟就好了。
19.4 单一数据源:为什么数据只能有一个「真相」
单一数据源,说白了就是「所有数据修改,必须经过同一个地方」。你不能在Activity里直接改数据库,又在Service里直接改网络缓存——那样数据会乱成一锅粥。
我一般把本地数据库作为单一数据源。网络数据拉下来后,先存到数据库,再从数据库读出来展示。这样无论数据从哪里来,最终都统一从数据库流向UI。
为什么选数据库而不是内存?因为数据库有持久化能力,App被杀后数据还在。而且Room自带Flow,天然支持响应式。
// 单一数据源的典型流程
// 1. ViewModel 订阅 Repository 的 Flow
// 2. Repository 从本地数据库发射数据
// 3. Repository 在后台刷新网络数据
// 4. 网络数据写入本地数据库
// 5. Room 自动通知 Flow 发射新数据
// 6. ViewModel 收到新数据,更新 UI
你想想看,如果ViewModel同时监听网络和数据库,那数据冲突了听谁的?谁才是「真相」?单一数据源就是解决这个问题的——数据库就是唯一的真相。
核心结论:
- Repository是数据访问的门面
- 数据源要拆成Local和Remote,各司其职
- 缓存策略要按业务场景选,不要一刀切
- 单一数据源选本地数据库,保证数据一致性
19.5 知识体系结构图
下面这张图,是我画的一个Repository模式的核心结构。你可以看到数据是怎么从网络和本地流向UI的:
这张图里,箭头方向代表数据流向。你看,ViewModel只跟Repository打交道,Repository内部协调三个数据源。LocalDataSource在最底下,它就是那个「单一真相」。
避坑指南:我曾经在一个项目里把CacheDataSource也当成「真相」之一,结果内存数据和数据库数据不一致,查了两天才找到原因。后来我统一规定:所有数据修改必须经过LocalDataSource,Cache只做读缓存,不做写操作。问题就再也没出现过。
Repository模式,说白了就是「高内聚、低耦合」在数据层的具体实践。它让数据访问变得可测试、可替换、可维护。你想想看,如果没有Repository,ViewModel里直接写网络请求和数据库操作——那代码得乱成什么样?
嗯,今天就聊到这里。记住一句话:Repository是数据层的门面,单一数据源是数据一致性的基石。