• Android Paging库使用详解


    Android分页包能够更轻易地在RecyclerView里面缓慢且优雅地加载数据.

    许多应用从数据源消耗数据, 数据源里面有大量的数据, 但是一次却只展示一小部分.

    分页包帮助应用观测和展示大量数据的合理数目的子集. 这个功能有如下几个优势:

    • 数据请求消耗更少的网络带宽和系统资源.
    • 即使在数据更新期间, 应用依然对用户输入响应迅速.

    添加分页依赖

    按照如下代码添加依赖:

     1 dependencies {
     2     def paging_version = "1.0.0"
     3 
     4     implementation "android.arch.paging:runtime:$paging_version"
     5 
     6     // alternatively - without Android dependencies for testing
     7     testImplementation "android.arch.paging:common:$paging_version"
     8 
     9     // optional - RxJava support, currently in release candidate
    10     implementation "android.arch.paging:rxjava2:1.0.0-rc1"
    11 }


    备注: 分页包帮助开发者在UI的列表容器中顺畅地展示数据, 而不管是使用设备内部的数据库还是从应用后端拉取数据.

    库架构

    分页库的核心构件是PagedList类, 它是一个集合, 用于异步加载应用数据块或者数据页. 该类在应用的其它架构之间充当中介.

    Data

    每一个PagedList实例从DataSource中加载最新的应用数据. 数据从应用后端或者数据库流入PagedList对象. 分页包支持多样的应用架构, 包括脱机数据库和与后台服务器通讯的数据库.

    UI

    PagedList类通过PagedListAdapter加载数据项到RecyclerView里面. 在加载数据的时候, 这些类协同工作, 拉取数据并展示内容, 包括预取看不见的内容并在内容改变时加载动画.

    支持不同的数据架构

    分页包支持应用架构, 包括应用拉取数据的地方是从后台服务器, 还是本机数据库, 还是两者的结合.

    只有网络

    要展示后台数据, 需要使用Retrofit的同步版本, 加载信息到自定义的DataSource对象中.
    备注: 分页包的DataSource对象并没有提供任何错误处理机制, 因为不同的应用需要用不同的方式处理和展示UI错误. 如果错误发生了, 顺从结果的回调, 然后稍后重试.

    只有数据库

    要设置RecyclerView观测本地存储, 偏向于使用Room持久化库. 用这种方式, 无论任何时候数据库数据插入或者修改, 这些改变会自动地在负责展示这些数据的RecyclerView展示出来.

    网络+数据库

    在开始观测数据库之后, 你能够通过使用PagedList.BoundaryCallback来监听数据库什么时候过期. 之后, 你可能从网络拉取更多的数据, 并把它们插入到数据库中. 如果UI正在展示数据库, 以上就是你所需要做的全部.
    下面的代码片断展示了BoundaryCallback的使用实例:

     1 class ConcertViewModel {
     2     fun search(query: String): ConcertSearchResult {
     3         val boundaryCallback =
     4                 ConcertBoundaryCallback(query, myService, myCache)
     5         // Error-handling not shown in this snippet.
     6         val networkErrors = boundaryCallback.networkErrors
     7     }
     8 }
     9 
    10 class ConcertBoundaryCallback(
    11         private val query: String,
    12         private val service: MyService,
    13         private val cache: MyLocalCache
    14 ) : PagedList.BoundaryCallback<Concert>() {
    15     override fun onZeroItemsLoaded() {
    16         requestAndSaveData(query)
    17     }
    18 
    19     override fun onItemAtEndLoaded(itemAtEnd: Concert) {
    20         requestAndSaveData(query)
    21     }
    22 }

    处理网络错误

    在使用网络拉取或者分页的数据, 而这些数据正在使用分页包展示的时候, 不总是把网络分为要么"可用"要么"不可能"是很重要的, 因为许多连接是间歇性或者成片的:

    • 特定的服务器可能不能响应网络请求;
    • 设备可能联接了慢的或者弱的网络;

    应用应该检查每一个请求是否成功, 并且在网络不可用的情形下, 尽可能快地恢复. 比如, 你可以为用户提供一个"重试"按钮, 如果数据没有刷新成功的话. 如果在数据分页期间发生错误, 最好自动地重新分页请求.

    更新已有应用

    如果应用已经从网络或者数据库消费数据, 很大可能可以直接升级到分页库提供的功能.

    自定义分页解决方案

    如果你使用了自定义功能加载数据源中的小的数据集, 你可以使用PagedList类取代这个逻辑. PagedList类实例提供了内建的连接, 到通用的数据源. 这些实例也提供了在应用中引用的RecyclerView的适配器.

    使用列表而非分页加载的数据

    如果你使用内存里的列表作为UI适配器的后备数据结构, 考虑使用PagedList类观测数据更新, 如果列表中数据项变得很多的话. PagedList实例既可以使用LiveData<PagedList>也可以使用Observable<List>对UI传递数据更新, 同时最小化了加载时间和内存使用. 然而, 应用中使用PagedList对象代替List并不要求对UI结构和数据更新逻辑作任何改变.

    使用CursorAdapter将数据cursor与列表视图联系起来

    应用也许会使用CursorAdapter将数据从Cursor跟ListView连接起来. 在这种情况下, 通常需要从ListView迁移到RecyclerView, 然后使用Room或者PositionalDataSource构件代替Cursor, 当然, 这主要依据于Cursor实例能否访问SQLite数据库.

    在一些情况下, 比如使用Spinner实例的时候, 你仅仅提供了Adapter本身. 然后一个库使用了加载进adapter中的数据, 并展示了数据. 在这些情况下, 把adapter数据类型转化为LiveData<PagedList>, 之后在尝试使用将这些数据项在UI中填充起来之前, 将这个列表在ArrayAdapter对象中包裹起来.

    使用AsyncListUtil异步加载内容

    如果你在使用AsyncListUtil对象异步地加载和展示分组信息的话, 分页包将会使得加载数据更加方便:

    • 数据并不需要定位. 分页包让你直接从后台使用网络提供的键加载数据.
    • 数据量太大. 使用分页包可以将数据加载分页直到没有任何数据留下.
    • 更方便地观测数据. 分页包能够展示应用在可观测数据结构中持有的ViewModel.

    数据库例子

    使用LiveData观测分页数据

    下面的示例代码展示了所有一起工作的碎片. 当演唱会事件在数据库中添加, 删除或者修改的修改的时候, RecyclerView中的内容自动且高效地更新:

     1 @Dao
     2 interface ConcertDao {
     3     // The Integer type parameter tells Room to use a PositionalDataSource
     4     // object, with position-based loading under the hood.
     5     @Query("SELECT * FROM user ORDER BY concert DESC")
     6     fun concertsByDate(): DataSource.Factory<Int, Concert>
     7 }
     8 
     9 class MyViewModel(concertDao: ConcertDao) : ViewModel() {
    10     val concertList: LiveData<PagedList<Concert>> = LivePagedListBuilder(
    11             concertDao.concertsByDate(),
    12             /* page size */ 20
    13     ).build()
    14 }
    15 
    16 class MyActivity : AppCompatActivity() {
    17     public override fun onCreate(savedState: Bundle?) {
    18         super.onCreate(savedState)
    19         val viewModel = ViewModelProviders.of(this)
    20                 .get(MyViewModel::class.java!!)
    21         val recyclerView = findViewById(R.id.concert_list)
    22         val adapter = ConcertAdapter()
    23         viewModel.concertList.observe(this, { pagedList ->
    24                 adapter.submitList(pagedList) })
    25         recyclerView.setAdapter(adapter)
    26     }
    27 }
    28 
    29 class ConcertAdapter() :
    30         PagedListAdapter<Concert, ConcertViewHolder>(DIFF_CALLBACK) {
    31     fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
    32         val concert = getItem(position)
    33         if (concert != null) {
    34             holder.bindTo(concert)
    35         } else {
    36             // Null defines a placeholder item - PagedListAdapter automatically
    37             // invalidates this row when the actual object is loaded from the
    38             // database.
    39             holder.clear()
    40         }
    41     }
    42 
    43     companion object {
    44         private val DIFF_CALLBACK = object : DiffUtil.ItemCallback<Concert>() {
    45             // Concert details may have changed if reloaded from the database,
    46             // but ID is fixed.
    47             override fun areItemsTheSame(oldConcert: Concert,
    48                     newConcert: Concert): Boolean =
    49                     oldConcert.id == newConcert.id
    50 
    51             override fun areContentsTheSame(oldConcert: Concert,
    52                     newConcert: Concert): Boolean =
    53                     oldConcert == newConcert
    54         }
    55     }
    56 }

    使用RxJava2观测分页数据

    如果你偏爱使用RxJava2而非LiveData, 那么你可以创建Observable或者Flowable对象:

    1 class MyViewModel(concertDao: ConcertDao) : ViewModel() {
    2     val concertList: Flowable<PagedList<Concert>> = RxPagedListBuilder(
    3             concertDao.concertsByDate(),
    4             /* page size */ 50
    5     ).buildFlowable(BackpressureStrategy.LATEST)
    6 }

    之后你可以按照如下代码开始和停止观测数据:

     1 class MyActivity : AppCompatActivity() {
     2     private lateinit var adapter: ConcertAdapter<Concert>
     3     private lateinit var viewModel: MyViewModel
     4 
     5     private val disposable = CompositeDisposable()
     6 
     7     public override fun onCreate(savedState: Bundle?) {
     8         super.onCreate(savedState)
     9         val recyclerView = findViewById(R.id.concert_list)
    10         viewModel = ViewModelProviders.of(this).get(MyViewModel::class.java!!)
    11         adapter = ConcertAdapter()
    12         recyclerView.setAdapter(adapter)
    13     }
    14 
    15     override fun onStart() {
    16         super.onStart()
    17         disposable.add(viewModel.concertList.subscribe({
    18                 flowableList -> adapter.submitList(flowableList)
    19         }))
    20     }
    21 
    22     override fun onStop() {
    23         super.onStop()
    24         disposable.clear()
    25     }
    26 }

    基于RxJava2解决方案的ConcertDao和ConcertAdapter代码, 和基于LiveData解决方案的代码是一样的.


    UI构件及其出发点

    将UI和视图模型联接起来

    你可以按照如下方式, 将LiveData<PagedList>实例跟PagedListAdapter联系起来:

    1 private val adapter = ConcertPagedListAdapter()
    2 private lateinit var viewModel: ConcertViewModel
    3 
    4 override fun onCreate(savedInstanceState: Bundle?) {
    5     viewModel = ViewModelProviders.of(this)
    6             .get(ConcertViewModel::class.java)
    7     viewModel.concerts.observe(this, adapter::submitList)
    8 }

    当数据源提供一个新PagedList实例的时候, activity会将这些对象改善给adapter. PagedListAdapter实现, 定义了更新如何计算, 自动地处理分页和列表不同. 由此, 你的ViewHolder只需要绑定到特定的提供项:

     1 class ConcertPagedListAdapter() : PagedListAdapter<Concert, ConcertViewHolder>(
     2         object : DiffUtil.ItemCallback<Concert>() {
     3     // The ID property identifies when items are the same.
     4     override fun areItemsTheSame(oldItem: Concert, newItem: Concert)
     5             = oldItem.id = newItem.id
     6 
     7     // Use the "==" operator (or Object.equals() in Java-based code) to know
     8     // when an item's content changes. Implement equals(), or write custom
     9     // data comparison logic here.
    10     override fun areContentsTheSame(oldItem: Concert, newItem: Concert) =
    11             oldItem.name == newItem.name && oldItem.date == newItem.date
    12     }
    13 ) {
    14     override fun onBindViewHolder(holder: ConcertViewHolder, position: Int) {
    15         val concert: Concert? = getItem(position)
    16 
    17         // Note that "concert" is a placeholder if it's null
    18         holder.bind(concert)
    19     }
    20 }

    PagedListAdapter使用PagedList.Callback对象处理分页加载事件. 当用户滑动时, PagedListAdapter调用PagedList.loadAround()方法将从DataSource中拉聚拢数据项提示提供给基本的PagedList.
    备注: PageList是内容不可变的. 这意味着, 尽管新内容能够被加载到PagedList实例中, 但已加载项一旦加载完成便不能发生改变. 由此, 如果PagedList中的内容发生改变, PagedListAdapter对象将会接收到一个包含已更新信息的全新的PagedList.

    实现diffing回调

    先前的代码展示了areContentsTheSame()的手动实现, 它比较了对象的相关的域. 你也可以使用Java中的Object.equals()方法或者Kotlin中的==操作符. 但是要确保要么实现了对象中的equals()方法或者使用了kotlin中的数据对象.

    使用不同的adapter类型进行diffing

    如果你选择不从PagedListAdapter继承--比如你在使用一个提供了自己的adapter的库的时候--你依然可以通过直接使用AsyncPagedListDiffer对象使用分页包adapter的diffing功能.

    在UI中提供占位符

    在应用完成拉取数据之前, 如果你想UI展示一个列表, 你可以向用户展示占位符列表项. RecyclerView通过将列表项临时地设置为null来处理这个情况.
    备注: 默认情况下, 分页包开启了占位符行为.
    占位符有如下好处:

    • 支持scrollbar. PagedList向PagedListAdapter提供了大量的列表项. 这个信息允许adapter绘制一个表示列表已满的scrollbar. 当新的页加载时, scrollbar并不会跳动, 因为列表是并不没有改变它的size.
    • 不需要"正在加载"旋转指针. 因为列表大小已知, 没必要提醒用户有更多的数据项正在加载. 占位符本身表达了这个信息.

    在添加占位符的支持之前, 请牢记以下先置条件:

    • 要求集合中数据可数. 来自Room持久化库的DataSource实例能够高效地计算数据项. 然而, 如果你在用自定义本地存储方案或者只有网络的数据架构, 想了解数据集中有多少数据项可能代价很高, 甚至不可能.
    • 要求adapter负责未加载数据项. 你正在使用的adapter或者展示机制来准备填充列表, 需要处理null列表项. 比如, 当将数据绑定到ViewHolder的时候, 你需要提供默认值表示未加载数据.
    • 要求数据相同数量的item view. 如果列表项数目能够基于内容发生改变, 比如, 社交网络更新, 交叉淡入淡出看起来并不好. 在这种情况下, 强烈推荐禁掉占位符.


    数据构件及其出发点

    构建可观测列表

    通常情况下, UI代码观测LiveData<PagedList>对象(或者, 如果你在使用RxJava2, 是Flowable<PagedList>/Observable<PagedList>对象), 这个对象存在于应用的ViewModel中. 这个可观测对象形成了应用列表数据内容和展示的连接.

    要创建这么一个可观测PagedList对象, 需要将DataSource.Factory实例传给LivePageListBuilder/RxPagedListBuilder对象. 一个DataSource对象对单个PagedList加载分页. 这个工厂类为内容更新创建PagedList实例, 比如数据库表验证, 网络刷新等. Room持久化库能够提供DataSource.Factory, 或者自定义.

    如下代码展示了如何在应用的ViewModel类中使用Room的DataSource.Factory构建能力创建新的LiveData<PagedaList>实例:
    ConcertDao.kt:

    1 interface ConcertDao {
    2     // The Integer type parameter tells Room to use a PositionalDataSource
    3     // object, with position-based loading under the hood.
    4     @Query("SELECT * FROM concerts ORDER BY date DESC")
    5     public abstract DataSource.Factory<Integer, Concert> concertsByDate()
    6 }

    ConcertViewModel.kt:

    1 // The Integer type argument corresponds to a PositionalDataSource object.
    2 val myConcertDataSource : DataSource.Factory<Integer, Concert> =
    3        concertDao.concertsByDate()
    4 
    5 val myPagedList = LivePagedListBuilder(myConcertDataSource, /* page size */ 20)
    6         .build()

    定义分页配置

    要想为复杂情形更深入地配置LiveData<PagedList>, 你也可以定义自己的分页配置. 尤其是, 你可以定义如下属性:

    • 页大小: 每一页的数据量.
    • 预取距离: 给定UI中最后可见项, 超过该项之后多少项, 分页包要尝试提前提取数据. 这个值应该比page size大几倍.
    • 占位符展示: 决定了UI是否会为还没有完成加载的数据项展示占位符.

    如果你想要对分布包从数据库加载中设置更多的控件, 要像下面的代码一样, 传递自定义的Executor对象给LivePagedListBuilder:
    EventViewModel.kt:

     1 val myPagingConfig = PagedList.Config.Builder()
     2         .setPageSize(50)
     3         .setPrefetchDistance(150)
     4         .setEnablePlaceholders(true)
     5         .build()
     6 
     7 // The Integer type argument corresponds to a PositionalDataSource object.
     8 val myConcertDataSource : DataSource.Factory<Integer, Concert> =
     9         concertDao.concertsByDate()
    10 
    11 val myPagedList = LivePagedListBuilder(myConcertDataSource, myPagingConfig)
    12         .setFetchExecutor(myExecutor)
    13         .build()

    选择正确的数据源类型

    连接更最好地处理源数据结构的数据源很重要:

    • 如果加载的页嵌套了之前/之后页的key的话, 使用PageKeyDataSource. 比如, 比如你正在从网络中拉取社交媒体博客, 你也许需要传递从一次加载向下一次加载的nextPage token.
    • 如果需要使用每N项数据项的数据拉取每N+1项的话, 使用ItemKeyedDataSource. 比如, 你在为一个讨论型应用拉取螺纹评论, 你可能需要传递最后一条评论的ID来获取下一条评论的内容.
    • 如果你需要从数据商店中的任意位置拉取分页数据的话, 使用PositionalDataSource. 这个类支持请求任意位置开始的数据集. 比如, 请求也许返回从位置1200开始的20条数据.

    通知数据非法

    在使用分页包时, 在表或者行数据变得陈腐时, 取决于数据层来通知应用的其它层. 要想这么做的话, 需要从DataSource类中调用invalidate()方法.
    备注: UI也可以使用"滑动刷新"模式来触发数据非法功能.

    构建自己的数据源

    如果你使用了自定义的数据解决方案, 或者直接从网络加载数据, 你可以实现一个DataSource子类. 下面的代码展示了数据源从给定的concert起始时间切断:

     1 class ConcertTimeDataSource(private val concertStartTime: Date) :
     2         ItemKeyedDataSource<Date, Concert>() {
     3     override fun getKey(item: Concert) = item.startTime
     4 
     5     override fun loadInitial(
     6             params: LoadInitialParams<Date>,
     7             callback: LoadInitialCallback<Concert>) {
     8         val items = fetchItems(concertStartTime, params.requestedLoadSize)
     9         callback.onResult(items)
    10     }
    11 
    12     override fun loadAfter(
    13             params: LoadParams<Date>,
    14             callback: LoadCallback<Concert>) {
    15         val items = fetchItemsAfter(
    16             date = params.key,
    17             limit = params.requestedLoadSize)
    18         callback.onResult(items)
    19     }
    20 }

    通过创建真实的DataSource.Factory子类, 你之后能够加载自定义的数据到PagedList对象. 下面的代码展示了如何创建在之前代码中定义的自定义数据源:

    1 class ConcertTimeDataSourceFactory(private val concertStartTime: Date) :
    2         DataSource.Factory<Date, Concert>() {
    3     val sourceLiveData = MutableLiveData<ConcertTimeDataSource>()
    4     override fun create(): DataSource<Date, Concert> {
    5         val source = ConcertTimeDataSource(concertStartTime)
    6         sourceLiveData.postValue(source)
    7         return source
    8     }
    9 }

    考虑内容更新

    当你构建可观测PagedList对象的时候, 考虑一下内容是如何更新的. 如果你直接从Room数据库中加载数据, 更新会自动地推送到UI上面.
    如果你在使用分页的网络API, 通常你会有用户交互, 比如"滑动刷新", 把它作为信号去验证当前DataSource非法并请求一个新的. 这个行为出行在下面的代码中:

     1 class ConcertActivity : AppCompatActivity() {
     2     override fun onCreate(savedInstanceState: Bundle?) {
     3         ...
     4 
     5         concertViewModel.refreshState.observe(this, Observer {
     6             swipeRefreshLayout.isRefreshing =
     7                     it == NetworkState.LOADING
     8         })
     9         swipeRefreshLayout.setOnRefreshListener {
    10             concertViewModel.invalidateDataSource()
    11         }
    12     }
    13 }

    提供数据表现之间的映射

    对于DataSource加载的数据, 分页包支持基于数据项和基于页的转换.
    下面的代码中, concert名和日期的联合被映射成包含姓名和日期的字符串:

     1 class ConcertViewModel : ViewModel() {
     2     val concertDescriptions : LiveData<PagedList<String>>
     3         init {
     4             val factory = database.allConcertsFactory()
     5                     .map { concert ->
     6                            concert.name + " - " + concert.date
     7                     }
     8             concerts = LivePagedListBuilder(factory, 30).build()
     9         }
    10     }
    11 }

    如果在数据加载之后, 想要包裹, 转换或者准备item, 这将非常有用. 因为这个工作是在获取执行器中完成的, 你可以在其中执行花销巨大的工作, 比如, 从硬盘中读取, 查询数据库等.
    备注: JOIN查询总是比作为map()一部分的查询要高效.

  • 相关阅读:
    [Python学习]Iterator 和 Generator的学习心得
    ubantu linux的bash shell初接触
    Linux-Ubuntu 启用root账户
    Ubuntu Linux系统三种方法添加本地软件库
    ASK,OOK,FSK的联系和区别
    spinlock一边连逻辑一边连控制器
    Cgroup与LXC简介
    关于 package.json 和 package-lock.json 文件说明
    ng build --aot 与 ng build --prod
    【Rxjs】
  • 原文地址:https://www.cnblogs.com/littlepanpc/p/9290289.html
Copyright © 2020-2023  润新知