本文译自「Incorporating the Repository Pattern into a Real-World Android」,原文链接https://medium.com/@siarhei.krupenich/incorporating-the-repository-pattern-into-a-real-world-android-app-739f2fee1460 ,由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 实例交互以访问数据,而所有底层复杂性都隐藏在其背后。这种方法完全符合我们的需求。
现在,让我们看一下代表该图的以下伪代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 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”版本。
现在,让我们实现该模式的增强版本:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// 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 并将其集成到应用程序中。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 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) 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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 ())
}
}
}
异常处理的扩展可能如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
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 )
)
}
结果错误处理扩展可以写成如下形式:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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
)
}
包装的故障数据类:
1
2
3
4
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 >()
}
并且转换映射扩展如下:
1
2
3
4
5
6
7
8
9
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 代码库:https://github.com/sergeykrupenich/TestRepo/tree/repository