1、介绍
你要建造什么
在这个代码库中,您从一个示例应用程序开始,该应用程序已经显示了GitHub存储库列表,从数据库加载数据并且由网络数据支持。 只要用户滚动并到达显示列表的末尾,就会触发新的网络请求,并将其结果保存在数据库中。
您将通过一系列步骤添加代码,在您进行时集成Paging库组件。 这些组件在步骤2中描述。
你需要什么
Android Studio 3.0或更高版本。
要熟悉以下架构组件:Room,LiveData,ViewModel以及“应用程序架构指南”中建议的架构。
2、设置您的环境
您还可以在GitHub上查看codelab。 初始状态位于主分支上,并在解决方案分支上查看解决方案。
3、分页库组件
分页库使您可以更轻松地在应用程序的UI中逐步和优雅地加载数据。
应用程序架构指南提出了一个具有以下主要组件的架构:
本地数据库,作为呈现给用户的数据的单一事实来源以及用户为更改该数据而采取的操作。
Web API服务
与数据库和API服务一起使用的存储库,提供统一的数据接口
ViewModel,提供特定于UI的数据
UI,显示ViewModel中数据的直观表示
Paging库可以处理所有这些组件并协调它们之间的交互,以便它可以从数据源中分页内容并在UI中显示该内容。
此代码库向您介绍了Paging库及其主要组件:
PagedList - 以异步方式在页面中加载数据的集合。 PagedList可用于从您定义的源加载数据,并使用RecyclerView在UI中轻松呈现。
DataSource和DataSource.Factory - DataSource是将数据快照加载到PagedList的基类。 DataSource.Factory负责创建DataSource。
LivePagedListBuilder - 基于DataSource.Factory和PagedList.Config构建LiveData 。
BoundaryCallback - 当PagedList到达可用数据的末尾时发出信号。
PagedListAdapter - 一个RecyclerView.Adapter,它在RecyclerView中显示来自PagedLists的分页数据。 PagedListAdapter在加载页面时监听PagedList加载回调,并在收到新的PagedLists时使用DiffUtil计算细粒度更新。
在此代码框中,您可以实现上述每个组件的示例。
4、工程概况
该应用程序允许您在GitHub中搜索其名称或描述包含特定单词的存储库。 将显示存储库列表,按降序排列,基于星号,然后按名称显示。 数据库是UI显示的数据的真实来源,它由网络请求支持。
按名称,通过RepoDao.reposByName中的LiveData对象检索存储库列表。 每当来自网络的新数据插入数据库时,LiveData将再次使用查询的整个结果发出。
当前实现有两个内存/性能问题:
一次加载数据库的整个repo表。
数据库的整个结果列表保存在内存中。
该应用程序遵循“应用程序架构指南”中推荐的体系结构,使用Room作为本地数据存储。 以下是每个包装中的内容:
api - 使用Retrofit包含Github API调用
db - 网络数据的数据库缓存
data - 包含存储库类,负责触发API请求并在数据库中保存响应
ui - 包含与使用RecyclerView显示活动相关的类
model - 包含Repo数据模型,它也是Room数据库中的一个表; 和
RepoSearchResult,UI用于观察搜索结果数据和网络错误的类
注意:GithubRepository和Repo类具有相似的名称,但用途却截然不同。 存储库类GithubRepository与代表GitHub代码存储库的Repo数据对象一起使用。
5、使用PagedList以块的形式加载数据
在我们当前的实现中,我们使用LiveData >从数据库中获取数据并将其传递给UI。 每当修改本地数据库中的数据时,LiveData都会发出更新的列表。 List 的替代方案是PagedList 。 PagedList是List的一个版本,它以块的形式加载内容。 与List类似,PagedList包含内容的快照,因此当通过LiveData传递PagedList的新实例时会发生更新。
创建PagedList时,它会立即加载第一个数据块,并在将来加载了内容时会再次扩展,。 PagedList的大小是每次传递期间加载的项目数。 该类支持无限列表和具有固定数量元素的非常大的列表。
用PagedList 替换List 的出现次数:
RepoSearchResult是UI用于显示数据的数据模型。 由于数据不再是LiveData >而是分页,因此需要将其替换为LiveData >。 在RepoSearchResult类中进行此更改。
SearchRepositoriesViewModel使用来自GithubRepository的数据。 更改ViewModel公开的repos val的类型,从LiveData >更改为LiveData >。
SearchRepositoriesActivity从ViewModel观察repos。 将List 中的观察者类型更改为PagedList 。
viewModel.repos.observe(this, Observer> {
showEmptyList(it?.size == 0)
adapter.submitList(it)
})
6、定义分页列表的数据源
agedList从源动态加载内容。 在我们的例子中,因为数据库是UI的主要真实来源,它也代表了PagedList的来源。 如果您的应用直接从网络获取数据并在没有缓存的情况下显示数据,则发出网络请求的类将成为您的数据源。
源由DataSource类定义。 要从可以更改的源(例如允许插入,删除或更新数据的源)中分页数据,您还需要实现知道如何创建DataSource的DataSource.Factory。 每当更新数据集时,DataSource都会失效并通过DataSource.Factory自动重新创建。
Room持久性库为与Paging库关联的数据源提供本机支持。 对于给定的查询,Room允许您从DAO返回DataSource.Factory并为您处理DataSource的实现。
更新代码以从Room获取DataSource.Factory:
RepoDao:更新reposByName()函数以返回DataSource.Factory 。
fun reposByName(queryString: String): DataSource.Factory
GithubLocalCache使用此功能。 将reposByName函数的返回类型更改为DataSource.Factory 。
7、构建和配置分页列表
要构建和配置LiveData ,请使用LivePagedListBuilder。 除了DataSource.Factory,您还需要提供PagedList配置,其中包括以下选项:
由PagedList加载的页面大小
加载到底有多远
第一次加载时要加载的项目数
是否要将空项添加到PagedList,以表示尚未加载的数据。
更新GithubRepository以构建和配置分页列表:
定义要由分页库检索的每页项目数。 在伴随对象中,添加另一个名为DATABASE_PAGE_SIZE的const val,并将其设置为20.然后,我们的PagedList将以20个项目的块为单位从DataSource中分页数据。
companion object {
private const val NETWORK_PAGE_SIZE = 50
private const val DATABASE_PAGE_SIZE = 20
}
注意:DataSource页面大小应该是几个屏幕的项目。 如果页面太小,您的列表可能会闪烁,因为页面内容未覆盖整个屏幕。 较大的页面大小有利于加载效率,但可能会增加显示更新的延迟。
在GithubRepository.search()方法中,进行以下更改:
删除lastRequestedPage初始化和对requestAndSaveData()的调用,但暂时不完全删除此函数。
创建一个新值以从cache.reposByName()中保存DataSource.Factory:
// Get data source factory from the local cache
val dataSourceFactory = cache.reposByName(query)
在search()函数中,从LivePagedListBuilder构造数据值。 LivePagedListBuilder是使用dataSourceFactory和您之前定义的数据库页面大小构造的。
fun search(query: String): RepoSearchResult {
// Get data source factory from the local cache
val dataSourceFactory = cache.reposByName(query)
// Get the paged list
val data = LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE).build()
// Get the network errors exposed by the boundary callback
return RepoSearchResult(data, networkErrors)
}
8、使RecyclerView适配器与PagedList一起使用
要将PagedList绑定到RecycleView,请使用PagedListAdapter。 每当加载PagedList内容时,PagedListAdapter都会收到通知,然后通知RecyclerView进行更新。
更新ReposAdapter以使用PagedList:
现在,ReposAdapter是一个ListAdapter。 使它成为PagedListAdapter:
class ReposAdapter : PagedListAdapter(REPO_COMPARATOR)
我们的应用程序最终编译! 运行它,看看它是如何工作的。
9、触发网络更新
目前,我们使用附加到RecyclerView的OnScrollListener来了解何时触发更多数据。 不过,我们可以让Paging库处理列表滚动。
删除自定义滚动处理:
SearchRepositoriesActivity:删除setupScrollListener()方法及其所有引用
SearchRepositoriesViewModel:删除listScrolled()方法和随播对象
删除自定义滚动处理后,我们的应用程序具有以下行为:
每当我们滚动时,PagedListAdapter都会尝试从特定位置获取项目。
如果该索引处的项目尚未加载到PagedList中,则Paging库会尝试从数据源获取数据。
当数据源没有任何更多数据要提供给我们时,会出现问题,因为从初始加载数据返回零项或者因为我们已经从DataSource到达数据的末尾。 要解决此问题,请实现BoundaryCallback。 当这两种情况发生时,该类会通知我们,因此我们知道何时请求更多数据。 由于我们的DataSource是一个由网络数据支持的Room数据库,因此回调告诉我们应该从API请求更多数据。
使用BoundaryCallback处理数据加载:
在数据包中,创建一个名为RepoBoundaryCallback的新类,该类实现PagedList.BoundaryCallback 。 因为此类处理特定查询的网络请求和数据库数据保存,所以将以下参数添加到构造函数:查询字符串,GithubService和GithubLocalCache。
在RepoBoundaryCallback中,重写onZeroItemsLoaded()和onItemAtEndLoaded()。
class RepoBoundaryCallback(
private val query: String,
private val service: GithubService,
private val cache: GithubLocalCache
) : PagedList.BoundaryCallback() {
override fun onZeroItemsLoaded() {
}
override fun onItemAtEndLoaded(itemAtEnd: Repo) {
}
}
将以下字段从GithubRepository移动到RepoBoundaryCallback:isRequestInProgress,lastRequestedPage和networkErrors。
从networkErrors中删除可见性修饰符。 为其创建支持属性,并将networkErrors的类型更改为LiveData 。 我们需要进行此更改,因为在内部,在RepoBoundaryCallback类中,我们可以使用MutableLiveData,但在类之外,我们只公开一个LiveData对象,其值无法修改。
// keep the last requested page.
// When the request is successful, increment the page number.
private var lastRequestedPage = 1
private val _networkErrors = MutableLiveData()
// LiveData of network errors.
val networkErrors: LiveData
get() = _networkErrors
// avoid triggering multiple requests in the same time
private var isRequestInProgress = false
在RepoBoundaryCallback中创建一个伴随对象,并在那里移动GithubRepository.NETWORK_PAGE_SIZE常量。
将GithubRepository.requestAndSaveData()方法移动到RepoBoundaryCallback。
更新requestAndSaveData()方法以使用支持属性_networkErrors。
每当Paging数据源通知我们源中没有项目(调用RepoBoundaryCallback.onZeroItemsLoaded()时)或者数据源中的最后一项已加载时,我们应该从网络请求数据并将其保存在缓存中( 当调用RepoBoundaryCallback.onItemAtEndLoaded()时)。 因此,从onZeroItemsLoaded()和onItemAtEndLoaded()调用requestAndSaveData()方法:
override fun onZeroItemsLoaded() {
requestAndSaveData(query)
}
override fun onItemAtEndLoaded(itemAtEnd: Repo) {
requestAndSaveData(query)
}
在创建PagedList时更新GithubRepository以使用BoundaryCallback:
在search()方法中,使用查询,服务和缓存构造RepoBoundaryCallback。
在search()方法中创建一个值,该值维护对RepoBoundaryCallback- 发现的网络错误的引用。
将边界回调设置为LivePagedListBuilder。
fun search(query: String): RepoSearchResult {
Log.d("GithubRepository", "New query: $query")
// Get data source factory from the local cache
val dataSourceFactory = cache.reposByName(query)
// Construct the boundary callback
val boundaryCallback = RepoBoundaryCallback(query, service, cache)
val networkErrors = boundaryCallback.networkErrors
// Get the paged list
val data = LivePagedListBuilder(dataSourceFactory, DATABASE_PAGE_SIZE)
.setBoundaryCallback(boundaryCallback)
.build()
// Get the network errors exposed by the boundary callback
return RepoSearchResult(data, networkErrors)
}
从GithubRepository中删除requestMore()函数
而已! 使用当前设置,Paging库组件是在正确的时间触发API请求,将数据保存在数据库中以及显示数据的组件。 因此,运行应用程序并搜索存储库。
10、包装
现在我们添加了所有组件,让我们退一步看看一切如何协同工作。
DataSource.Factory(由Room实现)创建DataSource。 然后,LivePagedListBuilder使用传入的DataSource.Factory,BoundaryCallback和PagedList配置构建LiveData 。 此LivePagedListBuilder对象负责创建PagedList对象。 创建PagedList时,会同时发生两件事:
LiveData将新的PagedList发送到ViewModel,ViewModel又将其传递给UI。 UI观察更改的PagedList并使用其PagedListAdapter更新呈现PagedList数据的RecyclerView。 (在下面的动画中用空方块表示)。
PagedList尝试从DataSource获取第一个数据块。 当DataSource为空时,例如,当应用程序第一次启动且数据库为空时,它会调用BoundaryCallback.onZeroItemsLoaded()。 在此方法中,BoundaryCallback从网络请求更多数据并将响应数据插入数据库中。
将数据插入DataSource后,将创建一个新的PagedList对象(由填充的方块在下面的动画中表示)。 然后,使用LiveData将此新数据对象传递给ViewModel和UI,并在PagedListAdapter的帮助下显示。
当用户滚动时,PagedList请求DataSource加载更多数据,查询数据库以获取下一个数据块。 当PagedList分页来自DataSource的所有可用数据时,会调用BoundaryCallback.onItemAtEndLoaded()。 BoundaryCallback从网络请求数据并将响应数据插入数据库中。 然后,基于新加载的数据重新填充UI。