在Android应用中实战Repository模式

本文译自「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()
        }
    }
}

存储库模式:优势与劣势

存储库的优势

  1. 保持井然有序------你的业务逻辑无需处理数据库查询。
  2. 易于测试------你可以将真实数据库与模拟数据库交换以进行测试。
  3. 面向未来------如果你从 SQLite 切换到 Firebase,只需更新存储库即可。
  4. 可重用------应用程序的不同部分可以使用相同的存储库,而无需编写重复的代码。
  5. 代码更简洁------它隐藏了复杂的查询,因此其余代码保持简洁。

为什么它可能很繁琐

  1. 增加额外代码------如果你的应用程序很小,使用存储库可能会有些过度。
  2. 可能会降低速度------更多的层级意味着更多的对象和方法调用。
  3. 缓存不是自动的------如果你想避免不必要的数据库调用,则需要付出额外的努力。
  4. 可能过于依赖数据模型------如果设计不当,更改数据库结构可能会很麻烦。
  5. 并非总是必要------有时,仅使用 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...

欢迎搜索并关注 公众号「稀有猿诉」 获取更多的优质文章!

保护原创,请勿转载!

相关推荐
幻雨様1 小时前
UE5多人MOBA+GAS 45、制作冲刺技能
android·ue5
Jerry说前后端3 小时前
Android 数据可视化开发:从技术选型到性能优化
android·信息可视化·性能优化
Meteors.4 小时前
Android约束布局(ConstraintLayout)常用属性
android
alexhilton5 小时前
玩转Shader之学会如何变形画布
android·kotlin·android jetpack
whysqwhw9 小时前
安卓图片性能优化技巧
android
风往哪边走9 小时前
自定义底部筛选弹框
android
Yyyy48210 小时前
MyCAT基础概念
android
Android轮子哥10 小时前
尝试解决 Android 适配最后一公里
android
雨白11 小时前
OkHttp 源码解析:enqueue 非同步流程与 Dispatcher 调度
android
风往哪边走12 小时前
自定义仿日历组件弹框
android