深入探索 Paging 3.0: 分页加载来自网络和数据库的数据 | MAD Skills
欢迎回到 MAD Skills 系列之 Paging 3.0!在上一篇文章《获取数据并绑定到 UI | MAD Skills》中,我们在 ViewModel
中集成了 Pager
,并利用配合 PagingDataAdapter
向 UI 填充数据,我们也添加了加载状态指示器,并在出现错误时重新加载。
这次,我们把难度提升一个档次。目前为止,我们都是直接通过网络加载数据,而这样的操作只适用于理想环境。我们有时候可能遇到网络连接缓慢,或者完全断网的情况。同时,即使网络状况良好,我们也不会希望自己的应用成为数据黑洞——在导航到每个界面时都拉取数据是一种十分浪费的行为。
解决这一问题的方法便是从 本地缓存 加载数据,并且只在必要的时候进行刷新。对缓存数据的更新必须先到达本地缓存,再传播至 ViewModel。这样一来,本地缓存便可成为唯一可信的数据源。对我们来说十分方便的是 Paging 库在 Room 库一些小小的帮助下已经可以应对这种场景。下面就让我们开始吧!点击这里 查看 Paging: 显示数据及其加载状态视频,了解更多详情。
使用 Room 创建 PagingSource
由于我们将要分页的数据源会来自本地而不是直接依赖 API,那么我们要做的第一件事便是更新 PagingSource
。好消息是,我们要做的工作很少。是因为我前面提到的 "来自 Room 的小小帮助" 吗?事实上这里的帮助远不止于一点: 只需要在 Room 的 DAO 中为 PagingSource
添加声明,便可通过 DAO
获取 PagingSource
!
@Dao interface RepoDao { @Query( "SELECT * FROM repos WHERE " + "name LIKE :queryString" ) fun reposByName(queryString: String): PagingSource<Int, Repo> }
我们现在可以在 GitHubRepository
中更新 Pager
的构造函数来使用新的 PagingSource
了:
fun getSearchResultStream(query: String): Flow<PagingData<Repo>> { … val pagingSourceFactory = { database.reposDao().reposByName(dbQuery) } @OptIn(ExperimentalPagingApi::class) return Pager( config = PagingConfig( pageSize = NETWORK_PAGE_SIZE, enablePlaceholders = false ), pagingSourceFactory = pagingSourceFactory, remoteMediator = …, ).flow }
RemoteMediator
目前为止一切顺利……不过我们好像忘记了什么。本地的数据库要如何填充数据呢?来看看 RemoteMediator,当数据库中的数据加载完毕时,它负责从网络加载更多数据。让我们看看它是如何工作的。
了解 RemoteMediator
的关键在于认识到它是一个回调。RemoteMediator
的结果永远不会展示在 UI 上,因为它只是 Paging 用于通知作为开发者的我们: PagingSource
的数据已经耗尽。更新数据库并通知 Paging,这是我们自己的工作。与 PagingSource
类似,RemoteMediator
有两个泛型参数: 查询参数类型和返回值类型。
@OptIn(ExperimentalPagingApi::class) class GithubRemoteMediator( … ) : RemoteMediator<Int, Repo>() { … }
让我们来仔细观察下 RemoteMediator
中的抽象方法。第一个方法是 initialize()
,它是在所有加载开始前,RemoteMediator
调用的第一个方法,它的返回值为 InitializeAction
。InitializeAction
可以是 LAUNCH_INITIAL_REFRESH
,也可以是 SKIP_INITIAL_REFRESH
。前者表示在调用 load() 方法时携带的加载类型为 refresh,后者意味着只有在 UI 明确发起请求时才会使用 RemoteMediator
执行刷新操作。在我们的用例中,由于仓库状态可能更新得颇为频繁,所以我们返回 LAUNCH_INITIAL_REFRESH
。
override suspend fun initialize(): InitializeAction { return InitializeAction.LAUNCH_INITIAL_REFRESH }
接下来我们来看 load
方法。load
方法在 loadType
与 PagingState
所定义的边界处调用,加载类型可以是 refresh
、append
或 prepend
。这一方法负责获取数据,将其持久化在磁盘上并通知处理结果,其结果可以是 Error
或 Success
。如果结果是 Error,加载状态将会反映这一结果,并可能重试加载。如果加载成功,需要通知 Pager
是否可以加载更多数据。
override suspend fun load(loadType: LoadType, state: PagingState<Int, Repo>): MediatorResult { val page = when (loadType) { LoadType.REFRESH -> … LoadType.PREPEND -> … LoadType.APPEND -> … } val apiQuery = query + IN_QUALIFIER try { val apiResponse = service.searchRepos(apiQuery, page, state.config.pageSize) val repos = apiResponse.items val endOfPaginationReached = repos.isEmpty() repoDatabase.withTransaction { … repoDatabase.reposDao().insertAll(repos) } return MediatorResult.Success(endOfPaginationReached = endOfPaginationReached) } catch (exception: IOException) { return MediatorResult.Error(exception) } catch (exception: HttpException) { return MediatorResult.Error(exception) } }
由于 load
方法是一个有返回值的挂起函数,所以 UI 可以精确地反映加载完成的状态。在上一篇文章中,我们简要介绍了 withLoadStateHeaderAndFooter
扩展函数,并了解了如何使用它来加载头部和底部。我们可以观察到,该扩展函数的名字中包含了一个类型: LoadState
。让我们进一步了解这一类型。
LoadState、LoadStates 以及 CombinedLoadStates
由于分页是一系列异步事件,所以通过 UI 反映加载数据的当前状态十分重要。在分页操作中,Pager
的加载状态是通过 CombinedLoadStates
类型表示的。
顾名思义,这个类型是其他表示加载信息的类型的组合。这些类型包括:
LoadState
是一个完整描述下列加载状态的密封类:
- Loading
- NotLoading
- Error
LoadStates
是包含以下三种 LoadState
值的数据类:
- append
- prepend
- refresh
通常来讲,prepend
与 append
加载状态会用于响应额外的数据获取,而 refresh 加载状态则用来响应初始加载、刷新和重试。
由于 Pager
可能会从 PagingSource
或者 RemoteMediator
加载数据,所以 CombinedLoadStates
有两个 LoadState
字段。其中名为 source
的字段用于 PagingSource
,而名为 mediator
的字段用于 RemoteMediator
。
方便起见,CombinedLoadStates
与 LoadStates
相似,同样含有 refresh
、append
和 prepend
字段,它们会基于 Paging
的配置和其他语义反映 RemoteMediator
或 PagingSource
的 LoadState
。请务必查看相关文档以确定这些字段在不同场景下的行为。
使用这些信息更新我们的 UI 就像从 PagingAdapter
暴露的 loadStateFlow
中获取数据一样简单。在我们的应用中,我们可以在第一次加载时使用这些信息显示一个加载指示器:
lifecycleScope.launch { repoAdapter.loadStateFlow.collect { loadState -> // 在刷新出错时显示重试头部,并且展示之前缓存的状态或者展示默认的 prepend 状态 header.loadState = loadState.mediator ?.refresh ?.takeIf { it is LoadState.Error && repoAdapter.itemCount > 0 } ?: loadState.prepend val isListEmpty = loadState.refresh is LoadState.NotLoading && repoAdapter.itemCount == 0 // 显示空列表 emptyList.isVisible = isListEmpty // 无论数据来自本地数据库还是远程数据,仅在刷新成功时显示列表。 list.isVisible = loadState.source.refresh is LoadState.NotLoading || loadState.mediator?.refresh is LoadState.NotLoading // 在初始加载或刷新时显示加载指示器 progressBar.isVisible = loadState.mediator?.refresh is LoadState.Loading // 如果初始加载或刷新失败,显示重试状态 retryButton.isVisible = loadState.mediator?.refresh is LoadState.Error && repoAdapter.itemCount == 0 } }
我们开始从 Flow
收集数据,并在 Pager
尚未加载且现存列表为空时,使用 CombinedLoadStates.refresh
字段展示进度条。我们之所以使用 refresh
字段,是因为我们只希望在第一次启动应用、或者明确触发了刷新时才展示大进度条。我们还可以检查是否有加载状态出错并通知用户。
回顾
在本文中,我们实现了以下功能:
- 使用数据库作为唯一可信数据源,并对数据进行分页;
- 使用 RemoteMediator 填充基于 Room 的 PagingSource;
- 使用来自 PagingAdapter 的 LoadStateFlow 更新带有进度条的 UI。
感谢您的阅读,下一篇文章将是 本系列 的最后一篇,敬请期待。
欢迎您 点击这里 向我们提交反馈,或分享您喜欢的内容、发现的问题。您的反馈对我们非常重要,感谢您的支持!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
Nebula Graph 源码解读系列 | Vol.06 MATCH 中变长 Pattern 的实现
目录 问题分析 定长 Pattern 变长 Pattern 与变长 Pattern 的组合 执行计划 拓展一步 拓展多步 保存路径 变长拼接 总结 MATCH 作为 openCypher 语言的核心,通过简洁的 Pattern 形式,可以让用户方便地表达图库中的关联关系。变长模式又是 Pattern 中用来描述路径的一种常用形式,对变长模式的支持是 Nebula 兼容 openCypher MATCH 功能的第一步。 由之前的系列文章可以了解到,Nebula 的执行计划是由许多的物理算子组成,每个算子都负责执行特有的计算逻辑,在 MATCH 的实现中也会涉及前述文章中的这些算子,比如 GetNeighbors、GetVertices、Join、Project、Filter、Loop 等等。因为 Nebula 的执行计划不同于关系数据库中的树状结构,在执行的流程上其实是一个有环的图。如何把 MATCH 中的变长 Pattern 变成 Nebula 的物理计划是 Planner 要解决的问题的重点。以下便简单介绍一下在 Nebula 中解决变长 Pattern 问题的思路。 问题分析 定长...
- 下一篇
OpenFaaS - 以自己的方式运行容器化函数
译者注: 本文篇幅较长,有助于了解 FaaS 和 OpenFaaS。作者分别从开发人员和运维人员的视角来了解 OpenFaaS,对了解新的技术是个很好的方式。 本文翻译自 Ivan Velichko 的 OpenFaaS - Run Containerized Functions On Your Own Terms。 长期以来,无服务器(serverless) 对我来说无非就是 AWS Lambda 的代名词。Lambda 提供了一种方便的途径,可以将任意代码附加到平台事件(云实例的状态变更、DynamoDB 记录的更新或新的 SNS 消息)中。但是,我时不时会想到某个逻辑,但其又没大到足以有自己的服务,同时有不适合任何现有服务的范围。因此,我经常将其放入函数中,以便日后使用 CLI 命令或者 HTTP 调用来调用它。 几年前,我来开了 AWS,自那以后,我一直怀念部署无服务器功能的便利性。因此,当我得知 OpenFaaS 项目时惊喜万分。它将在 Kubernetes 集群上部署函数变得简单,甚至仅需要 Containerd 就可以部署到虚拟机上。 有兴趣?那么继续! 无服务器与 Fa...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS关闭SELinux安全模块
- Windows10,CentOS7,CentOS8安装MongoDB4.0.16
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS8编译安装MySQL8.0.19
- Linux系统CentOS6、CentOS7手动修改IP地址
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Red5直播服务器,属于Java语言的直播服务器
- SpringBoot2整合Redis,开启缓存,提高访问速度
- CentOS7,8上快速安装Gitea,搭建Git服务器