本文译自「Incorporating the Repository Pattern into a Real-World Android」,原文链接medium.com/@siarhei.kr...,由Siarhei Krupenich发布于2025年4月4日。

引言
之前,我们探讨了整洁架构 (Clean Architecture) 中可能存在的问题,这些问题可能会损害 SOLID 原则、增加紧密耦合或使可测试性复杂化。我介绍了一种解决这些问题的解决方案,帮助我们维护更可扩展的代码库并更高效地添加新功能。在上一篇文章中,我还演示了如何将 ViewModel 拆分为两个主要部分------输入流和输出流------以减少紧密耦合并提高 ViewModel 的灵活性。
本文延续了这一思路,通过构建一个示例应用来展示 Android 开发的最佳实践,该应用通过 API 请求获取并显示 Git 仓库列表。我们的目标是将离线模式融入到项目中。实现这一目标最合适的方法是实现 Repository 模式。Repository 模式充当 Facade(如 GoF 设计模式中所述),在网络 API 和本地存储之间进行协调,确保高效的数据访问。
为了充分理解本文中的概念,建议你熟悉协程或 RxJava,尽管这些主题超出了本文的讨论范围。
存储库方法:离线模式的最佳解决方案
我们的目标是在应用中实现离线模式,确保即使在没有互联网连接的情况下也能访问数据。应用首次运行时,它会从网络获取数据,将其存储在缓存中,然后使用缓存的数据来最大限度地减少网络使用量并降低流量。
另一个关键方面是提供在需要时手动刷新网络数据的功能。最后,为了帮助用户在连接问题后识别应用何时恢复互联网连接,我们会在缓存数据旁边显示一条错误消息,直到连接恢复。
现在任务已经明确,让我们深入探讨即用型解决方案背后的理论。其核心是,我们需要解决经典的数据同步问题------从网络获取数据,将其存储在本地,并确保访问最新信息。
这意味着我们的应用至少需要两个数据源:一个用于网络 API 通信,另一个用于本地存储访问。根据应用的需求,可能会用到其他数据源,例如 Firebase(内置同步功能)、BLE 数据等等。
为了协调这些数据源(在我们的例子中是网络 API 和本地存储),最直观的设计模式是四人帮 (GoF) 的 Facade 模式。
Facade 模式是一种通过提供统一接口来简化与复杂系统交互的设计模式。在我们的例子中,这意味着我们可以将网络 API 和本地存储封装在一个抽象层之后。
我们新创建的 Facade 将同时保存 API 网络接口和本地存储接口的实例。另一方面,它将公开单一的访问方法,处理诸如强制从网络更新数据、本地存储数据以及管理错误等任务,同时隐藏内部复杂性。
我们来看看下面的图表:

该图呈现了一种简单的逻辑:客户端仅与Facade 实例交互以访问数据,而所有底层复杂性都隐藏在其背后。这种方法完全符合我们的需求。
现在,让我们看一下代表该图的以下伪代码:
Kotlin
// API Network
interface Network {
suspend fun obtainData(): Data
}
// Local Storage
interface Storage {
suspend fun save(data: Data)
suspend fun getData(): Data
}
// Facade
class DataFacade(
private val network: Network,
private val storage: Storage,
) {
suspend fun obtainData(): Data {
val data = network.obtainData()
storage.save(data)
return storage.getData()
}
}
// Client
class Client() {
fun main(){
testScope.launch {
val data = facade.obtainData()
}
}
}
这个简单的例子演示了这种方法:客户端持有一个 DataFacade 实例,并调用其obtainData() 方法。当然,在实际场景中,obtainData() 方法会包含更复杂的逻辑------处理错误、映射数据、将结果包装到 Result 类中,以及决定是获取新数据还是使用缓存版本。
现在,让我们更进一步,将这个 Facade 转换为 Repository 类。 Repository 模式旨在通过清晰的接口管理数据访问,同时隐藏底层的复杂性。从客户端的角度来看, 没有任何变化------用法保持不变------但在内部,逻辑结构良好且封装完整。
现在,让我们通过下图来查看 Repository 模式的结构:

上图表明,Repository 模式有效地捕捉了 Facade 模式;然而,"Repository"一词更能体现访问数据的逻辑, 因此我们将使用"Repository"版本。
现在,让我们实现该模式的增强版本:
Kotlin
// Repository Interface
interface DataRepository {
suspend fun obtainData(): Data
}
// Repository
class DataRepositoryImpl(
private val network: Network,
private val storage: Storage,
) : DataRepository {
override suspend fun obtainData(): Data {
val data = network.obtainData()
storage.save(data)
return storage.getData()
}
}
// Client
class Client() {
fun main() {
testScope.launch {
val data = repository.obtainData()
}
}
}
存储库模式:优势与劣势
存储库的优势
- 保持井然有序------你的业务逻辑无需处理数据库查询。
- 易于测试------你可以将真实数据库与模拟数据库交换以进行测试。
- 面向未来------如果你从 SQLite 切换到 Firebase,只需更新存储库即可。
- 可重用------应用程序的不同部分可以使用相同的存储库,而无需编写重复的代码。
- 代码更简洁------它隐藏了复杂的查询,因此其余代码保持简洁。
为什么它可能很繁琐
- 增加额外代码------如果你的应用程序很小,使用存储库可能会有些过度。
- 可能会降低速度------更多的层级意味着更多的对象和方法调用。
- 缓存不是自动的------如果你想避免不必要的数据库调用,则需要付出额外的努力。
- 可能过于依赖数据模型------如果设计不当,更改数据库结构可能会很麻烦。
- 并非总是必要------有时,仅使用 DAO 就足够了。
Repository 模式非常适合保持简洁性和可扩展性,但它并非总是最简单的选择。如果你的应用规模较小,跳过它可能会更轻松。然而,我们的重点是为快速增长且可扩展的应用提供解决方案,因此我们选择了它。
现在,是时候编写代码并增强 Repo 应用了。
首先,让我们改进 Repository 结构并使其适应应用。由于没有太多细节需要整合,因此最终的图表与之前的版本非常相似:

开撸
接下来,让我们实现 Repository 并将其集成到应用程序中。
Kotlin
// The Repository pattern implementation
internal class ReposRepositoryImpl @Inject constructor(
private val reposNetworkApi: ReposNetworkApi,
private val reposDao: ReposDao,
// mappers may be placed here
) : ReposRepository {
override suspend fun getRepos(i
sRefreshing: Boolean
): ResultWithFallback<List<Repo>> {
return runCatching {
val dbRepos = reposDao.getReposWithRelations()
if (isRefreshing) {
reposNetworkApi.getRepos().fold({ result ->
reposDao.insertReposWithRelations(result)
ResultWithFallback.Success(
reposDao.getReposWithRelations()
)
}, { error ->
error.errorToResultWithFallback(dbRepos)
})
} else {
ResultWithFallback.Success(dbRepos)
}
}.getOrElse { error ->
error.exceptionToResultWithFallback(
reposDao.getReposWithRelations()
)
}
}
override suspend fun clearRepos() {
reposDao.clearRepos()
}
}
该代码片段演示了如何将 Repository 模式集成到应用中。它包含两个方法:一个用于清除数据,另一个用于获取数据。getRepos(isRefreshing: Boolean) 方法包含一个标志,用于强制从网络刷新数据。同时,它也可能从缓存中返回数据(例如,Room DB 用作缓存)。如果发生错误,即使数据已缓存,该方法也会返回一个包含失败信息的响应。
由于我们主要关注的是协程,因此让我们使用 RxJava 重写 getRepos(isRefreshing: Boolean) 方法:
Kotlin
override fun getRepos(
isRefreshing: Boolean
): Single<ResultWithFallback<List<Repo>>> {
return if (isRefreshing) {
reposNetworkApi.getRepos()
.flatMap { result ->
reposDao.insertReposWithRelations(networkReposToDbReposMapper.map(result))
reposDao.getReposWithRelations()
.map { dbReposToReposMapper.map(it) }
.map { ResultWithFallback.Success(it) }
}
.onErrorResumeNext { error ->
reposDao.getReposWithRelations()
.map { error.errorToResultWithFallback(it) }
}
} else {
reposDao.getReposWithRelations()
.map { dbReposToReposMapper.map(it) }
.map { ResultWithFallback.Success(it) }
.onErrorReturn { error ->
error.exceptionToResultWithFallback(emptyList())
}
}
}
异常处理的扩展可能如下所示:
Kotlin
fun Throwable.exceptionToResultWithFallback(
data: List<RepoWithRelations>,
): ResultWithFallback.Failure<List<Repo>> = if (data.isNotEmpty()) {
ResultWithFallback.Failure(
dbReposToReposMapper.map(data), RepoError.Unknown(this)
)
} else {
ResultWithFallback.Failure<List<Repo>>(
data = null,
RepoError.Unknown(this)
)
}
结果错误处理扩展可以写成如下形式:
Kotlin
fun Throwable.errorToResultWithFallback(
localData: List<RepoWithRelations>
): ResultWithFallback.Failure<List<Repo>> {
val repoError = when (this) {
is IOException -> RepoError.NetworkLost
is HttpException ->
if (code() == 401)
RepoError.Unauthorized
else RepoError.Unknown(this)
else -> RepoError.Unknown(this)
}
return ResultWithFallback.Failure(
dbReposToReposMapper.map(localData),
repoError
)
}
包装的故障数据类:
Kotlin
sealed class ResultWithFallback<out T> {
data class Success<T>(val data: T) : ResultWithFallback<T>()
data class Failure<T>(val data: T?, val error: RepoError) : ResultWithFallback<T>()
}
并且转换映射扩展如下:
Kotlin
inline fun <T, R> ResultWithFallback<T>.map(transform: (T) -> R): ResultWithFallback<R> {
return when (this) {
is ResultWithFallback.Success -> ResultWithFallback.Success(transform(data))
is ResultWithFallback.Failure -> ResultWithFallback.Failure(
data?.let { transform(it) },
error
)
}
}
结论
我们已成功将存储库模式(Repository pattern)集成到应用中,事实证明,这是一种维护离线模式的绝佳方法。此模式不仅简化了数据管理,还确保了可扩展性。它是实现数据检索和存储功能的最有效方法之一,随着项目规模的增长,你可以更轻松地管理本地和远程数据源。
你可以通过以下链接探索与本文主题相关的 GitHub 代码库:github.com/sergeykrupe...
欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!
保护原创,请勿转载!