在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...

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

保护原创,请勿转载!

相关推荐
小镇学者2 小时前
【PHP】导入excel 报错Trying to access array offset on value of type int
android·php·excel
一笑的小酒馆5 小时前
Android11 Launcher3去掉抽屉改为单层
android
louisgeek7 小时前
Git 根据不同目录设置不同账号
android
qq_390934748 小时前
MySQL中的系统库(简介、performance_schema)
android·数据库·mysql
whysqwhw9 小时前
Kotlin Flow 实现响应式编程指南
android
二流小码农9 小时前
鸿蒙开发:一文了解桌面卡片
android·ios·harmonyos
每次的天空9 小时前
Android第十七次面试总结(Java数据结构)
android·java·面试
梁同学与Android9 小时前
Android --- Handler的用法,子线程中怎么切线程进行更新UI
android·handler·子线程更新ui·切换到主线程
Fastcv9 小时前
这TextView也太闪了,咋做的?
android
恋猫de小郭10 小时前
iOS 26 beta1 重新禁止 JIT 执行,Flutter 下的 iOS 真机 hot load 暂时无法使用
android·前端·flutter