Android Paging Lib codeLab

混江龙づ霸主 提交于 2020-01-11 07:24:21

Android Paging

1. Introduction

What you build

在此代码实验室中,您可以从一个示例应用程序开始,该应用程序已显示GitHub存储库列表,从数据库加载数据,并由网络数据支持。每当用户滚动到显示列表的末尾时,就会触发一个新的网络请求,并将其结果保存到支持该列表的数据库中。

您将通过一系列步骤添加代码,并在进程中集成Paging Library组件。步骤2中描述了这些组件。

What you'll need

  • Android studio 3.4 or higher
  • Familiarity with the following Architecture Components: Room, LiveData, ViewModel and with the architecture suggested in the "Guide to App Architecture".

For an introduction to Architecture Components, check out the Room with a View codelab.

2.Setup Your Environment

在这个步骤中,您将下载整个codelab的代码,然后运行一个简单的示例应用程序。

为了让您尽快开始工作,我们准备了一个入门项目,供您在此基础上进行。

如果安装了git,只需运行下面的命令即可。(您可以通过在终端/命令行中键入git-版本来进行检查,并验证它是否正确执行。)

 git clone https://gitee.com/endian11/AndroidPagingLib.git

The app runs and displays a list of GitHub repositories similar to this one:

 

3. Paging library components

Paging Library使您更容易在应用程序的UI中逐步地、优雅地加载数据。

The Guide to App Architecture proposes an architecture with the following main components:

  • A local database that serves as a single source of truth for data presented to the user and manipulated by the user.
  • A web API service.
  • A repository that works with the database and the web API service, providing a unified data interface.
  • A ViewModel that provides data specific for the UI.
  • The UI, which shows a visual representation of the data in the ViewModel.

分页库与所有这些组件一起工作,并协调它们之间的交互,以便从数据源加载内容的“页面”,并在UI中显示该内容。

This codelab introduces you to the Paging library and its main components:

  • PagedList - 异步加载页中数据的集合。可以使用PagedList从您定义的源加载数据,并在您的UI中轻松地使用RecyclerView显示数据 .
  • DataSource and DataSource.Factory - DataSource是将数据快照加载到PagedList的基类。工厂负责创建DataSource。
  • LivePagedListBuilder - builds a LiveData, based on DataSource.Factory and a PagedList.Config.
  • BoundaryCallback - 分页列表已达到可用数据的结尾时的信号。
  • PagedListAdapter -RecyclerView.Adapter,用于在RecyclerView中显示PagedLists中的分页数据。PagedListAdapter在加载页面时侦听PagedList加载回调,并在收到新的PagedList时使用DiffUtil计算细粒度更新

在这个codelab中,您实现了上述每个组件的示例。

4. Project overview

该应用程序允许您搜索其名称或说明包含特定单词的存储库的gibthub。根据星星的数量,按降序显示存储库的列表,然后按名称显示。数据库是UI显示的数据的真实来源,它由网络请求支持

The list of repository names is retrieved via a LiveData object in RepoDao.reposByName. Whenever new data from the network is inserted into the database, the LiveData will emit the entire result of the query. The current implementation has two memory/performance issues:

  • The entire repo table of the database is loaded at once.
  • The entire list of results from the database is kept in memory.

The app follows the architecture recommended in the "Guide to App Architecture", using Room as local data storage. Here's what you will find in each package:

  • api - contains Github API calls, using Retrofit
  • db - database cache for network data
  • data - contains the repository class, responsible for triggering API requests and saving the response in the database
  • ui - contains classes related to displaying an Activity with a RecyclerView
  • model - contains the Repo data model, which is also a table in the Room database; and RepoSearchResult, a class that is used by the UI to observe both search results data and network errors
  • Caution: The GithubRepository and Repo classes have similar names but serve very different purposes. The repository class, GithubRepository, works with Repo data objects that represent GitHub code repositories.

5. Load data in chunks with the PagedList

在当前的实现方式中,我们使用 LiveData<List>从数据库获取数据并将其传递给UI。每当本地数据库中的数据被修改时,LiveData会发出更新的列表。

List<Repo>列表的替代方法是PagedList<Repo>。PagedList是以块加载内容的列表的一个版本。与列表类似,PagedList保存内容快照,因此当PagedList的新实例通过LiveData传递时会发生更新。

创建PagedList时,它会立即加载第一个数据块,并随着内容在以后的传递中加载而扩展。PagedList的大小是每次传递期间加载的项数。该类既支持无限列表,也支持具有固定数量元素的非常大的列表。

Replace occurrences of List with PagedList:

  • model.RepoSearchResult is the data model that's used by the UI to display data. Since the data is no longer a LiveData<List<Repo>> but is paginated, it needs to be replaced with LiveData<PagedList<Repo>>. Make this change in the RepoSearchResult class.

  • ui.SearchRepositoriesViewModel works with the data from the GithubRepository. Change the type of the repos val exposed by the ViewModel, from LiveData<List<Repo>> to LiveData<PagedList<Repo>>.

  • ui.SearchRepositoriesActivity observes the repos from the ViewModel. Change the type of the observer from List<Repo> to PagedList.

    viewModel.repos.observe(this, Observer<PagedList<Repo>> {
                showEmptyList(it?.size == 0)
                adapter.submitList(it)
     })
    

6. Define the source of data for the paged list

PagedList从源动态加载内容。在我们的例子中,因为数据库是UI的主要真理来源,所以它也代表了PagedList的源。如果应用程序直接从网络获取数据并在不缓存的情况下显示数据,那么发出网络请求的类将是您的数据源。

数据源由DataSource类定义。要从可以更改的源(例如允许插入、删除或更新数据的源)页面中的数据,还需要实现一个知道如何创建DataSource的DataSource.Factory。每当更新数据时,DataSource就会失效,并通过DataSource.Factory自动重新创建。

Room持久性库为与Paging Library关联的数据源提供了本机支持。对于给定的查询,Room允许您从DAO返回DataSource.Factory并为您处理DataSource的实现

Update the code to get a DataSource.Factory from Room:

  • db.RepoDao: update the reposByName() function to return a DataSource.Factory<Int, Repo>.

    fun reposByName(queryString: String): DataSource.Factory<Int, Repo>
    
  • db.GithubLocalCache uses this function. Change the return type of reposByName function to DataSource.Factory<Int, Repo>.

7. Build and configure a pagedlist

要构建和配置一个LiveData<PagedList>,请使用LivePagedListBuilder。除了DataSource.Factory之外,您还需要提供一个PagedList配置,它可以包括以下选项:

  • the size of a page loaded by a PagedList
  • how far ahead to load
  • how many items to load when the first load occurs
  • whether null items can be added to the PagedList, to represent data that hasn't been loaded yet. Update data.GithubRepository to build and configure a paged list:

Define the number of items per page, to be retrieved by the paging library. In the companion object, add another const val called DATABASE_PAGE_SIZE, and set it to 20. Our PagedList will then page data from the DataSource in chunks of 20 items.

    companion object {
            private const val NETWORK_PAGE_SIZE = 50
            private const val DATABASE_PAGE_SIZE = 20
    }

Note:DataSource页面大小应该是几个屏幕上的项的值。如果页面太小,您的列表可能会闪烁,因为页面内容没有覆盖整个屏幕。较大的页面大小有利于提高加载效率,但在更新列表时会增加延迟。.

 data.GithubRepository.search()方法中, 作出以下修改:

  • Remove the lastRequestedPage initialization and the call to requestAndSaveData(), but don't completely remove this function for now.
  • Create a new value to hold the DataSource.Factory from cache.reposByName():

    // Get data source factory from the local cache
    val dataSourceFactory = cache.reposByName(query)
    
  • In the search() function, construct the data value from a LivePagedListBuilder. The LivePagedListBuilder is constructed using the dataSourceFactory and the database page size that you each defined earlier.

    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. Make the RecyclerView Adapter work with a PagedList

若要将PagedList绑定到RecycleView,请使用PagedListAdapter。每当加载PagedList内容时,PagedListAdapter就会得到通知,然后向RecyclerView发出更新信号

Update the ui.ReposAdapter to work with a PagedList:

  • 现在,ReposAdapter是ListAdapter。使其成为PagedListAdapter:

    class ReposAdapter : PagedListAdapter<Repo, RecyclerView.ViewHolder>(REPO_COMPARATOR)
    

我们的应用程序最终编译!运行它,并检查它的工作原理。

9. Trigger network updates

目前,我们使用附加到RecyclerView的OnScrollListener来知道何时触发更多数据。不过,我们可以让Paging library处理列表滚动。

Remove the custom scroll handling:

  • ui.SearchRepositoriesActivity: remove the setupScrollListener() method and all references to it
  • ui.SearchRepositoriesViewModel: remove the listScrolled() method and the companion object

After removing the custom scroll handling, our app has the following behavior:

  • 当我们滚动时,PageedListAdapter将尝试从特定位置获取该项目(item)。
  • 如果该索引处的item尚未加载到PagedList中,则分页库将尝试从数据源获取数据

当数据源没有更多的数据提供给我们时,就会出现一个问题,要么是因为从初始数据请求中返回了零项,要么是因为我们已经从DataSource返回了数据的末尾。若要解决此问题,请实现BoundaryCallback。当这两种情况发生时,该类都会通知我们,因此我们知道何时请求更多数据。因为我们的DataSource是一个由网络数据支持的Room数据库,回调让我们知道我们应该从API中请求更多的数据。

Handle data loading with BoundaryCallback:

  • 在data包中,创建一个名为RepoBoundaryCallback 的新的类,该回调实现了pagedList.boundaryCallback。因为该类处理特定查询的网络请求和数据库数据保存,因此将以下参数添加到构造函数:查询字符串(String)、githubService和githulocalCache。
  • 在RepoBoundaryCallback中,重写onZeroItemsLoaded()和onItemAtEndLoaded()。

    class RepoBoundaryCallback(
            private val query: String,
            private val service: GithubService,
            private val cache: GithubLocalCache
    ) : PagedList.BoundaryCallback<Repo>() {
        override fun onZeroItemsLoaded() {
        }
    
        override fun onItemAtEndLoaded(itemAtEnd: Repo) {
        }
    }
    
  • 将以下字段从GithubRepository移动到RepoBoundaryCallback:isRequestInProgress、lastRequestedPage和networkErrors。

  • 从networkErrors删除可见性修饰符。为它创建一个支持属性,并将NetworkError的类型更改为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<String>()
    // LiveData of network errors.
    val networkErrors: LiveData<String>
         get() = _networkErrors
    
    // avoid triggering multiple requests in the same time
    private var isRequestInProgress = false
    
  • 在RepoBoundaryCallback中创建一个伴随对象,并在其中移动GithubRepository.NETWORK_PAGE_SIZE常量。

  • 将GithubRepository.requestAndSaveData()方法移动到RepoBoundaryCallback
  • 更新 requestAndSaveData() 方法以使用支持属性_networkErrors.
  • 当寻呼数据源通知我们源中没有项时,我们应该从网络请求数据,并将其保存在缓存中(当调用RepobeliaryCallback.onZeroItemsLoade()时),或者加载数据源的最后一个项(调用RepobeliaryCallback.onItemAtEndLoade()时)。因此,从onZeroItemsLoade()和onItemAtEndLoade()调用requestAndSaveData()方法:

    override fun onZeroItemsLoaded() {
        requestAndSaveData(query)
    }
    
    override fun onItemAtEndLoaded(itemAtEnd: Repo) {
        requestAndSaveData(query)
    }
    

Update data.GithubRepository to use the BoundaryCallback when creating the PagedList:

  • 在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 library组件是在正确的时间触发API请求、将数据保存在数据库中并显示数据的组件。所以,运行应用程序并搜索repositories。

10. Wrap up

现在我们添加了所有组件,让我们后退一步,看看所有组件是如何一起工作的。

DataSource.Factory工厂(由Room实现)创建DataSource。然后,LivePagedListBuilder使用传入的DataSource.Factory、BoundaryCallback和PagedList配置构建LiveData。这个LivePagedListBuilder对象负责创建PagedList对象。创建PagedList时,同时发生两件事:

LiveData向ViewModel发送新的PagedList,ViewModel又将其传递给UI。UI观察更改的PagedList并使用其PagedListAdapter更新显示PagedList数据的RecyclerView。(PagedList在下面的动画中以一个空方形表示)。PagedList尝试从DataSource获取第一个数据块。当DataSource为空时,例如,当应用程序首次启动并且数据库为空时,它调用BoundaryCallback.onZeroItemsLoaded()。在此方法中,BoundaryCallback从网络请求更多数据并将响应数据插入数据库中。

在DataSource中插入数据后,将创建一个新的PagedList对象(在下面的动画中由填充方格表示)。然后使用LiveData将这个新的数据对象传递给ViewModel和UI,并在PagedListAdapter的帮助下显示。

当用户滚动时,PagedList请求DataSource加载更多数据,查询数据库中的下一个数据块。当PagedList从DataSource分页所有可用数据时,会调用BoundaryCallback.onItemAtEndLoaded()。BoundaryCallback从网络中请求数据,并将响应数据插入数据库中。然后,根据新加载的数据重新填充UI。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!