存储库模式

1. 准备工作

简介

在本 Codelab 中,您将使用离线缓存来改善应用的用户体验。许多应用都会依赖网络数据。如果您的应用在每次启动时都从服务器中提取数据,让用户看到加载屏幕,就可能使用户体验不佳,从而导致用户卸载您的应用。

用户在启动应用时,都希望应用能够快速显示数据。如果实现离线缓存,您便能达成此目标。离线缓存是指应用将从网络中提取的数据保存到设备的本地存储空间,从而提高访问速度。

由于应用将能通过网络获取数据,并且会离线缓存之前下载的结果,因此您需要让应用通过某种方式来整理这些来自多个来源的数据。为此,您可以实现一个存储库类,将其作为应用数据的单一可信来源,并从视图模型中提取出数据源(网络、缓存等)。

您应当已掌握的内容

您应熟悉以下内容/操作:

学习内容

  • 如何实现存储库,以便从应用的其余部分中提取应用的数据层。
  • 如何使用存储库加载缓存的数据。

实践内容

  • 使用存储库提取数据层,并将存储库类与 ViewModel 集成。
  • 显示离线缓存中的数据。

2. 起始代码

下载项目代码

请注意,文件夹名称为 RepositoryPattern-Starter。在 Android Studio 中打开项目时,请选择此文件夹。

如需获取此 Codelab 的代码并在 Android Studio 中打开它,请执行以下操作。

获取代码

  1. 点击提供的网址。此时会在浏览器中打开项目的 GitHub 页面。
  2. 检查并确认分支名称与 Codelab 中指定的分支名称匹配。例如,在以下屏幕截图中,分支名称为 main

8cf29fa81a862adb.png

  1. 在该项目的 GitHub 页面上,点击 Code 按钮,以打开一个弹出式窗口。

1debcf330fd04c7b.png

  1. 在弹出式窗口中,点击 Download ZIP 按钮,将项目保存到计算机上。等待下载完成。
  2. 在计算机上找到该文件(可能在 Downloads 文件夹中)。
  3. 双击 ZIP 文件进行解压缩。系统将创建一个包含项目文件的新文件夹。

在 Android Studio 中打开项目

  1. 启动 Android Studio。
  2. Welcome to Android Studio 窗口中,点击 Open

d8e9dbdeafe9038a.png

注意:如果 Android Studio 已经打开,则改为依次选择 File > Open 菜单选项。

8d1fda7396afe8e5.png

  1. 在文件浏览器中,转到解压缩的项目文件夹所在的位置(可能在 Downloads 文件夹中)。
  2. 双击该项目文件夹。
  3. 等待 Android Studio 打开该项目。
  4. 点击 Run 按钮 8de56cba7583251f.png 以构建并运行应用。请确保该应用按预期构建。

3. 起始应用概览

DevBytes 应用会以 Recycler 视图的形式列出 YouTube 上 Android Developers 频道中的 DevBytes 视频,用户在此处点击即可打开指向对应视频的链接

9757e53b89d2de7c.png

尽管起始代码已能正常运行,但存在一个重大缺陷,可能会对用户体验产生负面影响。如果用户的网络连接不稳定,或者根本没有互联网连接,该应用将不会显示任何视频。即使应用之前打开过,也会出现这种情况。如果用户退出后又重新启动应用,但这次没有网络,那么即使应用尝试重新下载视频列表,也不会成功。

您可以在模拟器中查看此操作的实际运行情况。

  1. 在 Android 模拟器中暂时开启飞行模式(依次点击 Settings App > Network & Internet > Airplane mode)。
  2. 运行 DevBytes 应用,您会观察到一个空白屏幕。

f0365b27d0dd8f78.png

  1. 务必先关闭飞行模式,然后再继续本 Codelab 的其余部分。

之所以出现这种情况,是因为 DevBytes 应用在首次下载数据后,并未缓存任何数据以供日后使用。该应用当前包含一个 Room 数据库。您的任务是使用这个数据库来实现缓存功能,并更新视图模型来使用存储库。该存储库将会下载新数据,或从 Room 数据库中提取相关数据。存储库类会将此逻辑从视图模型中提取出来,以保持代码的条理性和解耦性。

起始项目由多个软件包组成。

25b5f8d0997df54c.png

我们欢迎并鼓励您自行熟悉该代码,您在这里只会接触到 repository/VideosRepository.ktviewmodels/DevByteViewModel 这两个文件。首先,您将创建一个 VideosRepository 类来实现用于缓存的存储库模式(在接下来的几页中,我们会进行详细介绍),然后更新 DevByteViewModel 以使用新的 VideosRepository 类。

不过,在查看代码之前,让我们花点时间详细了解一下缓存和存储库模式。

4. 缓存和存储库模式

存储库

存储库模式是一种将数据层与应用的其余部分隔离开来的设计模式。数据层是指独立于界面的那部分应用,用于处理应用的数据和业务逻辑,从而为应用的其余部分访问该数据提供一致的 API。界面负责向用户显示信息,数据层负责包含网络代码、Room 数据库、错误处理,以及任何会读取或操纵数据的代码等等。

9e528301efd49aea.png

存储库可以解决数据源(例如持久性模型、网络服务和缓存)之间的冲突,和集中管理对这些数据做出的更改。以下示意图就展示了应用组件(如 activity)如何通过存储库与数据源进行交互。

69021c8142d29198.png

如需实现存储库,请使用单独的类,例如您在下一个任务中创建的 VideosRepository 类。存储库类会将数据源与应用的其余部分隔离开来,并提供一个干净的 API 来访问应用其余部分中的数据。使用存储库类可确保此代码与 ViewModel 类是分开的,是推荐为代码分离和架构采用的最佳做法。

使用存储库的优势

存储库模块可以处理数据操作,并且允许您使用多个后端。在典型的实际应用中,存储库可实现对以下任务做出决定时所需的逻辑:是从网络中提取数据,还是使用缓存在本地数据库中的结果。您可以使用存储库来更换实现细节(例如迁移到其他持久性库),而不会影响发起调用的代码(如视图模型)。它还有助于模块化您的代码,使其易于测试。您可以轻松模块化存储库并测试其余代码。

存储库应作为特定应用数据的单一可信来源。在使用多个数据源(例如联网资源和离线缓存)时,存储库可确保应用的数据尽可能准确并保持最新状态,即使应用处于离线状态,也会尽量提供最佳体验。

缓存

缓存是指用于存储应用所用数据的存储空间。例如,您可能希望暂时保存来自网络的数据,以防用户的互联网连接中断。即使网络不再可用,应用仍然可以依靠缓存的数据。缓存还有助于为不再位于屏幕上的 activity 存储临时数据,甚至在两次应用启动之间保留数据。

缓存可以采用多种形式,有些较简单,有些较复杂,视具体任务而定。下表列出了在 Android 中实现网络缓存的几种方式。

缓存方法

用途

Retrofit 是一个网络库,用于为 Android 实现类型安全的 REST 客户端。您可以配置 Retrofit,以为每个网络结果在本地存储一个副本。

对于简单的请求和响应、低频率网络调用或小型数据集,这是个不错的选择。

您可以使用 DataStore 来存储键值对。

如果键很少并且值较简单(例如应用设置),这是个不错的选择。您无法使用此方法来存储大量结构化数据。

您可以访问应用的内部存储空间目录,然后在其中保存数据文件。应用的软件包名称会指定该应用的内部存储空间目录(即 Android 文件系统中的一个特定位置)。此目录供您的应用专用,并且会在应用卸载后被清除。

如果您有文件系统可以满足的特定需求(例如,您需要保存媒体文件或数据文件,并且必须自行管理这些文件),这是个不错的选择。您无法使用此方法来存储应用需要查询的复杂结构化数据。

您可以使用 Room 缓存数据。Room 是一个 SQLite 对象映射库,它会在 SQLite 的基础上提供一个抽象层。

对于复杂的结构化可查询数据,这是个不错的选择,因为在设备文件系统上存储结构化数据的最佳方法就是存储在本地 SQLite 数据库中。

在本 Codelab 中,您将使用 Room,因为它是在设备文件系统上存储结构化数据的推荐方法。DevBytes 应用已配置为使用 Room。您的任务是,使用存储库模式实现离线缓存,以将数据层与界面代码分开。

5. 实现 VideosRepository

任务:创建存储库

在此任务中,您将创建一个存储库来管理您已在上一个任务中实现的离线缓存。您的 Room 数据库没有用于管理离线缓存的逻辑,只有用来插入、更新、删除和检索数据的方法。存储库会存在一个逻辑来提取网络结果,并及时更新数据库。

第 1 步:添加存储库

  1. repository/VideosRepository.kt 中,创建一个 VideosRepository 类。传入 VideosDatabase 对象作为类的构造函数参数,以访问 DAO 方法。
class VideosRepository(private val database: VideosDatabase) {
}
  1. VideosRepository 类中,添加一个名为 refreshVideos()suspend 方法。该方法没有参数且不返回任何内容,是用于刷新离线缓存的 API。
suspend fun refreshVideos() {
}
  1. refreshVideos() 方法内,将协程上下文切换为 Dispatchers.IO,以执行网络和数据库操作。
suspend fun refreshVideos() {
   withContext(Dispatchers.IO) {
   }
}
  1. withContext 代码块内,使用 Retrofit 服务实例 DevByteNetwork 从网络中提取 DevByte 视频播放列表。
val playlist = DevByteNetwork.devbytes.getPlaylist()
  1. refreshVideos() 方法中,从网络获取播放列表后,将该播放列表存储到 Room 数据库中。如需存储播放列表,请使用 VideosDatabase 类。调用 insertAll() DAO 方法,传入从网络检索到的播放列表。使用 asDatabaseModel() 扩展函数将播放列表映射到数据库对象。
database.videoDao.insertAll(playlist.asDatabaseModel())
  1. 以下是完整的 refreshVideos() 方法,其中包含跟踪何时该方法被调用的日志语句:
suspend fun refreshVideos() {
   withContext(Dispatchers.IO) {
       val playlist = DevByteNetwork.devbytes.getPlaylist()
       database.videoDao.insertAll(playlist.asDatabaseModel())
   }
}

第 2 步:从数据库中检索数据

在此步骤中,您将创建一个 LiveData 对象,用于从数据库中读取视频播放列表。在更新数据库时,此 LiveData 对象会自动更新。相关的 fragment(或 activity)会使用新值进行刷新。

  1. VideosRepository 类中,声明一个名为 videosLiveData 对象,以存储 DevByteVideo 对象的列表。使用 database.videoDao 作为 videos 对象的初始值,并调用 getVideos() DAO 方法。由于 getVideos() 方法返回的是数据库对象列表,而不是 DevByteVideo 对象列表,因此 Android Studio 会抛出“type mismatch”错误。
val videos: LiveData<List<DevByteVideo>> = database.videoDao.getVideos()
  1. 若要修正此错误,请使用 Transformations.map 通过 asDomainModel() 转换函数将数据库对象列表转换为网域对象列表。
val videos: LiveData<List<DevByteVideo>> = Transformations.map(database.videoDao.getVideos()) {
   it.asDomainModel()
}

现在,您已经为应用实现了存储库。在下一个任务中,您将使用一个简单的刷新策略,让本地数据库保持最新状态。

6. 在 DevByteViewModel 中使用 VideosRepository

任务:使用刷新策略集成存储库

在此任务中,您会使用一个简单的刷新策略将存储库与 ViewModel 集成,并会显示 Room 数据库中的视频播放列表,而不是直接从网络中提取。

数据库刷新是更新或刷新本地数据库以与网络数据保持同步的过程。在此示例应用中,您将使用一个简单的刷新策略,其中向存储库请求数据的模块负责刷新本地数据。

在实际应用中,这类策略可能会更复杂。例如,代码可能会在后台自动刷新数据(需考虑带宽)或缓存用户接下来最有可能使用的数据。

  1. viewmodels/DevByteViewModel.ktDevByteViewModel 类中,创建一个名为 videosRepository 且类型为 VideosRepository 的私有成员变量。通过传入单例 VideosDatabase 对象来实例化该变量。
private val videosRepository = VideosRepository(getDatabase(application))
  1. DevByteViewModel 类中,将 refreshDataFromNetwork() 方法替换为 refreshDataFromRepository() 方法。旧方法 refreshDataFromNetwork() 是使用 Retrofit 库从网络中提取视频播放列表,该新方法则是从存储库加载视频播放列表。存储库会确定从哪个来源(例如网络、数据库等)检索播放列表,而无需在视图模型中包含相应实现细节。存储库还能让您的代码更易于维护;即使您日后要更改获取数据的实现方式,也无需修改视图模型。
private fun refreshDataFromRepository() {
   viewModelScope.launch {
       try {
           videosRepository.refreshVideos()
           _eventNetworkError.value = false
           _isNetworkErrorShown.value = false

       } catch (networkError: IOException) {
           // Show a Toast error message and hide the progress bar.
           if(playlist.value.isNullOrEmpty())
               _eventNetworkError.value = true
       }
   }
}
  1. DevByteViewModel 类的 init 代码块中,将函数调用从 refreshDataFromNetwork() 更改为 refreshDataFromRepository()。此代码可从存储库(而不是直接从网络)中提取视频播放列表。
init {
   refreshDataFromRepository()
}
  1. DevByteViewModel 类中,删除 _playlist 属性及其后备属性 playlist

要删除的代码

private val _playlist = MutableLiveData<List<Video>>()
...
val playlist: LiveData<List<Video>>
   get() = _playlist
  1. DevByteViewModel 类中,在实例化 videosRepository 对象后,添加一个名为 playlist 的新 val,来存储存储库中的 LiveData 视频列表。
val playlist = videosRepository.videos
  1. 运行应用。应用会像以前一样运行,但现在,DevBytes 播放列表是从网络中提取并保存在 Room 数据库中。屏幕上显示的播放列表是取自 Room 数据库,而不是直接来自网络。

30ee74d946a2f6ca.png

  1. 您只需在模拟器或设备上启用飞行模式,就能看到这种差异。
  2. 再次运行应用。请注意,应用不会显示“网络连接错误”消息框消息,而是显示从离线缓存中提取的播放列表。
  3. 在模拟器或设备中关闭飞行模式。
  4. 关闭并重新打开应用。应用会从离线缓存中加载播放列表,而在后台运行网络请求。

如果新数据来自网络,屏幕便会自动进行更新,以显示新数据。不过,DevBytes 服务器不会刷新其内容,因此您看不到数据更新。

太棒了!在本 Codelab 中,您已将离线缓存与 ViewModel 集成,从而通过存储库来显示播放列表,而不是从网络中提取该播放列表。

7. 解决方案代码

解决方案代码

Android Studio 项目:RepositoryPattern

8. 恭喜

恭喜!在本开发者在线课程中,您学习了以下内容:

  • 缓存是将从网络中提取的数据存储到设备存储空间的过程。通过缓存,您的应用可在设备处于离线状态时,或者需要再次访问相同的数据时,访问相应数据。
  • 让应用将结构化数据存储在设备文件系统上的最佳方法是使用本地 SQLite 数据库。Room 是一个 SQLite 对象映射库,这意味着它会在 SQLite 的基础上提供一个抽象层。在实现离线缓存时,使用 Room 是推荐采用的最佳做法。
  • 存储库类会将数据源(例如 Room 数据库和网络服务)与应用的其余部分隔离开来,并提供一个干净的 API 来访问应用其余部分中的数据。
  • 对于代码分离和架构,使用存储库是推荐采用的最佳做法。
  • 在设计离线缓存时,最佳做法是将应用的网络、网域和数据库对象分离开。此策略就是一个关注点分离的例子。

了解更多内容