欢迎大家来到IT世界,在知识的湖畔探索吧!

欢迎大家来到IT世界,在知识的湖畔探索吧!
欢迎回到 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
}
欢迎大家来到IT世界,在知识的湖畔探索吧!
我们现在可以在 GitHubRepository 中更新 Pager 的构造函数来使用新的 PagingSource 了:
欢迎大家来到IT世界,在知识的湖畔探索吧!fun getSearchResultStream(query: String): Flow<PagingData
> { … 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
() { … }
让我们来仔细观察下 RemoteMediator 中的抽象方法。第一个方法是 initialize(),它是在所有加载开始前,RemoteMediator 调用的第一个方法,它的返回值为 InitializeAction。InitializeAction 可以是 LAUNCH_INITIAL_REFRESH,也可以是 SKIP_INITIAL_REFRESH。前者表示在调用 load() 方法时携带的加载类型为 refresh,后者意味着只有在 UI 明确发起请求时才会使用 RemoteMediator 执行刷新操作。在我们的用例中,由于仓库状态可能更新得颇为频繁,所以我们返回 LAUNCH_INITIAL_REFRESH。
欢迎大家来到IT世界,在知识的湖畔探索吧! 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
): 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 中获取数据一样简单。在我们的应用中,我们可以在第一次加载时使用这些信息显示一个加载指示器:
欢迎大家来到IT世界,在知识的湖畔探索吧!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。
感谢您的阅读,下一篇文章将是本系列的最后一篇,敬请期待。
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://itzsg.com/117901.html