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 缓存策略:怎么让数据又快又新鲜

缓存策略,说白了就是「什么时候读本地,什么时候读网络」。我常用的策略有三种:

  1. 网络优先:每次先请求网络,成功就更新本地,失败就读本地缓存。适合实时性要求高的场景,比如股票行情。
  2. 缓存优先:先读本地,有数据就直接返回,后台静默刷新。适合用户资料、配置信息这类不常变的数据。
  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的:

Repository模式核心结构 ViewModel Repository RemoteDataSource LocalDataSource CacheDataSource 单一数据源:LocalDataSource 作为唯一真相来源 缓存策略:网络数据先写入 Local,再从 Local 读取

这张图里,箭头方向代表数据流向。你看,ViewModel只跟Repository打交道,Repository内部协调三个数据源。LocalDataSource在最底下,它就是那个「单一真相」。

避坑指南:我曾经在一个项目里把CacheDataSource也当成「真相」之一,结果内存数据和数据库数据不一致,查了两天才找到原因。后来我统一规定:所有数据修改必须经过LocalDataSource,Cache只做读缓存,不做写操作。问题就再也没出现过。

Repository模式,说白了就是「高内聚、低耦合」在数据层的具体实践。它让数据访问变得可测试、可替换、可维护。你想想看,如果没有Repository,ViewModel里直接写网络请求和数据库操作——那代码得乱成什么样?

嗯,今天就聊到这里。记住一句话:Repository是数据层的门面,单一数据源是数据一致性的基石


公众号:蓝海资料掘金营,微信deep3321