基于Android官方Paging Library的RecyclerView分页加载框架

一、概述
在2018年5月9日的谷歌开发者大会(Google I/O 2018) 中提出在去年发布的广受欢迎的架构组件上,进一步改进并推出了Jetpack。Jetpack能帮助我们更专注提升应用体验,加快应用开发速度,处理类似后台任务、UI 导航以及生命周期管理等。发布的新版 Android Jetpack 组件中更新的内容包括 4 个部分:WorkManager、Paging、Navigation 以及 Slices。我们今天要说的就是Paging,在进行大数据查询的时候,Paging分页组件可以让我们从本地或者网络中通过渐进的方式、逐步的请求数据加载,在不过多增加设备负担或等待时间的情况下,让应用拥有了处理大型数据的能力,其中包括对RecycleView的支持。和往常一样,主要是想总结一下Android官方Paging Library的学习过程以及一些需要注意的地方。
详细请查看谷歌官方文档:https://developer.android.com/topic/libraries/architecture/paging/

二、Paging Libray介绍
2.1、Paging Library中DataSource,PagedList,PagedAdapter三者之间的关系以及数据加载到数据展示的流程:
(1)如下图所示,Paging的数据流是在后台线程生产的,在后台线程中完成了大部分工作,在UI线程中显示。
比如说:当一条新的item插入到数据库,DataSource会被初始化,LiveData<PagedList>后台线程就会创建一个新的PagedList。这个新的PagedList会被发送到UI线程的PagedListAdapter中,PagedListAdapter使用DiffUtil在对比现在的Item和新建Item的差异。当对比结束,PagedListAdapter通过调用RecycleView.Adapter.notifyItemInserted()将新的item插入到适当的位置。RecycleView就会知道它需要绑定一个新的item,并将其显示。
(2)从代码层面来说,我们需要给Recyclerview设置PagedListAdater,PagedListAdapter设置对应的PagedList。每一次adapter getItem就是让PagedList知道我们已经滑到第几个item了,PagedList计算这些数量以及配置的参数,当条件达成就通知DataSource,让其返回数据。数据返回成功时,通知PagedListAdapter进行刷新等操作。

2.2、Datasource
Datasource是数据源相关的类,针对不同场景,Paging 提供了三种 Datasource:
(1)PageKeyedDataSource:如果页面在加载时插入一个/下一个键,例如:从网络获取社交媒体的帖子,可能需要将nextPage加载到后续的加载中;

(2)ItemKeyedDataSource:在需要让使用的数据的item从N条增加到N+1条时使用;

(3)PositionalDataSource:如果需要从数据存储的任意位置来获取数据页面。此类支持你从任何位置开始请求一组item的数据集。例如,该请求可能会返回从位置1200条开始的20个数据项;

根据使用场景选择实现DataSource不同的抽象类, 使用时需实现请求加载数据的方法。其中这三种Datasource 都需要实现 loadInitial()方法, 各自都封装了请求初始化数据的参数类型 LoadInitialParams。 不同的是分页加载数据的方法, PageKeyedDataSource和 ItemKeyedDataSource比较相似, 需要实现 loadBefore()和 loadAfter()方法,同样对请求参数做了封装,即 LoadParams<Key>。 PositionalDataSource需要实现 loadRange()

2.3、PagedList
PagedList 通过 Datasource 加载数据, 通过 Config 的配置,可以设置一次加载的数量以及预加载的数量等。 除此之外,PagedList 还可以向 RecyclerView.Adapter 发送更新UI的信号。
Config: 配置 PagedList 从 Datasource 加载数据的方式, 其中包含以下属性:

  • setPageSize:设置每一页加载的数量;
  • setInitialLoadSizeHint:设置首次加载的数量;
  • setPrefetchDistance:设置距离最后还有多少个item时,即寻呼库开始加载下一页的数据;
  • setEnablePlaceholders:表示是否设置null占位符;

2.4、PagedListAdapter
PagedListAdapter 是 RecyclerView.Adapter 的实现,用于展示 PagedList 的数据。PagedListAdapter中每一次getItem就是让PagedList知道我们已经滑到第几个item了,当数据源提供新的PagedList时,PagedList会计算这些数量以及配置的参数,当条件达成就通知DataSource,让其返回数据。数据返回成功时,通知PagedListAdapter。 而PagedListAdapter会使用DiffUtil再对比现在的Item和新建Item的差异并进行刷新等操作

三、加载的数据源类型
数据的加载方式主要有两种:
3.1、单一数据源:本地数据或网络数据
首先我们可以通过 LivePagedListBuilder来创建 LiveData<PagedList>为 UI 层提供数据。如果数据源是DB,当数据发生变化,DB 会推送一个新的 PagedList(依赖LiveData的机制)。如果是网络数据,因为我们无法知道数据源的变化,以通过滑动刷新的方式调用 Datasource 的 invalidate()方法来加载新的数据。流程如下所示:

image

3.2、多个数据源:本地数据+网络数据
一般是先加载本地数据,加载完成后再加载网络数据。比如 IM 中的聊天消息,当打开聊天界面时先加载本地数据库中的聊天消息,加载完了再加载网络的离线消息。这个时候我们需要为 PagedList 设置 BoundaryCallback来监听本地数据是否加载完成,当本地数据加载完成就触发加载网络数据,然后入库,此时LiveData会推送一个新的PagedList, 并触发界面刷新。
image

四、代码的实现
按照惯例,我们先来看看效果图
image

从效果图中可以看到,当RecyclerView不断下滑时,就触发分页加载,把RecyclerView后续要使用的数据分页加载显示出来。这么说吧,当我们滑动第一页时,在还没有滑到底部的时候,它将请求第二页的数据,代码中就是实现ItemKeyedDataSource中的loadAfter方法;当我们滑动第二页时,在还没有滑到底部的时候,它将请求第三页的数据,以此类推。好了,下面我们来看看代码吧
1、首先要使用Android Paging Library,我们需要在应用的build.gradle中添加Paging支持库以及我在Dome中用到的其他库的引用(注意:这篇文章是基于Paging Library 1.0.0的版本。):

    //paging
    implementation 'android.arch.paging:runtime:1.0.0'
    // RxKotlin
    implementation 'io.reactivex.rxjava2:rxkotlin:2.2.0'
    // RxAndroid
    implementation 'io.reactivex.rxjava2:rxandroid:2.0.2'
    // Lifecycle
    implementation 'android.arch.lifecycle:extensions:1.1.1'

2、StudentDataSourceFactory:
为了创建这些可观察 PagedList对象,我们需要将DataSource.Factory给一个LivePagedListBuilder对象。一个DataSource对象为单个页面加载PagedList数据。工厂类创建新的实例PagedList以响应内容更新,例如数据库表失效和网络刷新。

package per.lijuan.pagingdome.paging

import android.arch.lifecycle.MutableLiveData
import android.arch.paging.DataSource
import java.util.concurrent.Executor

/**
 * 一个简单的数据源工厂,它提供了一种观察上次创建的数据源的方式,这使得我们能够将其网络请求状态等返回到UI界面
 * Created by juan on 2018/05/23.
 */
class StudentDataSourceFactory(private val retryExecutor: Executor): DataSource.Factory<String, StudentBean>() {
    val sourceLiveData=MutableLiveData<StudentDataSource>()
    override fun create(): DataSource<String, StudentBean> {
        val source= StudentDataSource(retryExecutor)
        sourceLiveData.postValue(source)
        return source
    }
}

3、StudentDataRepository:

package per.lijuan.pagingdome.paging

import android.arch.lifecycle.Transformations.switchMap
import android.arch.paging.LivePagedListBuilder
import android.arch.paging.PagedList
import java.util.concurrent.Executor

/**
 1. Repository实现返回一个直接从网络加载数据的Listing,并使用该名称作为加载上一页/下一页数据的关键
 2. Created by juan on 2018/05/23.
 */
class StudentDataRepository(private val executor: Executor): StudentRepository {
    override fun getStudentList(pageSize: Int): Listing<StudentBean?> {
        val sourceFactory= StudentDataSourceFactory(executor)
        val pagedListConfig = PagedList.Config.Builder()
                .setEnablePlaceholders(false)
                .setInitialLoadSizeHint(pageSize*2)//定义第一页加载项目的数量
                .setPageSize(pageSize)//定义从DataSource中每一次加载的项目数量
                .build()
        val pagedList = LivePagedListBuilder(sourceFactory, pagedListConfig)
                .setFetchExecutor(executor)//设置Executor执行器用于从用于从DataSources中获取PagedLists数据,如果未设置,则默认为Arch组件I/O线程。
                .build()
        val refreshState = switchMap(sourceFactory.sourceLiveData) {
            it.initialLoad
        }
        return Listing<StudentBean?>(
                pagedList =pagedList,
                networkState = switchMap(sourceFactory.sourceLiveData, {
                    it.networkState
                }),
                retry = {
                    sourceFactory.sourceLiveData.value?.retryAllFailed()
                },
                refresh = {
                    sourceFactory.sourceLiveData.value?.invalidate()
                },
                refreshState = refreshState)
    }
}

4、定义一个用于给不同的Repository实现共享的通用接口

/**
 3. 给不同的Repository实现共享的通用接口
 4. Created by juan on 2018/05/23.
 */
interface StudentRepository {
    fun getStudentList(pageSize: Int): Listing<StudentBean?>
}

5、选择正确的数据源类型
下面,我们就使用DataSource建立自己的分页数据源,其中包括定义加载第一页以及后面每一页数据

package per.lijuan.pagingdome.paging

import android.arch.lifecycle.MutableLiveData
import android.arch.paging.ItemKeyedDataSource
import android.util.Log
import java.util.concurrent.Executor

/**
 * DataSource负责加载第一页以及后面每一页数据
 * Created by juan on 2018/05/23.
 */
open class StudentDataSource(private val retryExecutor: Executor) : ItemKeyedDataSource<String, StudentBean>(){
    private var TAG: String="paging"
    private var retry:(()->Any)?=null
    private var startPosition:Int = 0

    fun retryAllFailed(){
        val prevRetry=retry
        retry=null
        prevRetry?.let {
            retryExecutor.execute { it.invoke() }
        }
    }

    val networkState by lazy{
        MutableLiveData<Resource<String>>()
    }
    val initialLoad by lazy{
        MutableLiveData<Resource<String>>()
    }

    /**
     * 接收初始加载的数据,在这里需要将获取到的数据通过LoadInitialCallback的onResult进行回调,用于出始化PagedList,并对加载的项目进行计数
     */
    override fun loadInitial(params: LoadInitialParams<String>, callback: LoadInitialCallback<StudentBean>) {
        Log.d(TAG,"loadInitial->mSkip:"+startPosition+",count:"+params.requestedLoadSize)
        networkState.postValue(Resource.loading(null))
        initialLoad.postValue(Resource.loading(null))

        //模拟耗时操作
        val list=loadData(startPosition,params.requestedLoadSize)
        retry=null
        networkState.postValue(Resource.success(null))
        initialLoad.postValue(Resource.success(null))
        callback.onResult(list)
        startPosition+=list.size
    }

    /**
     * 接收加载的数据
     */
    override fun loadAfter(params: LoadParams<String>, callback: LoadCallback<StudentBean>) {
        Log.d(TAG,"loadAfter->mSkip:"+startPosition+",count:"+params.requestedLoadSize)
        networkState.postValue(Resource.loading(null))

        //模拟耗时操作
        val list=loadData(startPosition,params.requestedLoadSize)
        retry=null
        networkState.postValue(Resource.success(null))
        callback.onResult(list)
        startPosition+=list.size
    }

    override fun loadBefore(params: LoadParams<String>, callback: LoadCallback<StudentBean>) {
        Log.d(TAG,"loadBefore")
    }

    override fun getKey(item: StudentBean): String  = item.id!!

    /**
     * 模拟耗时操作,假设这里需要做一些后台线程的数据加载任务。
     */
    private fun loadData(startPosition: Int, limit: Int): List<StudentBean> {
        val list = ArrayList<StudentBean>()

        for (i in 0 until limit) {
            var position=startPosition + i
            val data = StudentBean(position.toString(), "学生@$position")
            list.add(data)
        }
        return list
    }
}

这里的实现了ItemKeyedDataSource中几个方法:
(1)loadInitial(@NonNull LoadInitialParams<Key> params,@NonNull LoadInitialCallback<Value> callback)
->用于接收初始第一页加载的数据,在这里需要将获取到的数据通过LoadInitialCallback的onResult进行回调,用于出始化PagedList,并对加载的项目进行计数

(2)loadAfter(@NonNull LoadParams<Key> params,@NonNull LoadCallback<Value> callback)
->用于接收后面每一页加载的数据,使用方法和loadInitial一样

(3)loadBefore(@NonNull LoadParams<Key> params,@NonNull LoadCallback<Value> callback)
->指定的密钥之前加载列表数据

(4)getKey(@NonNull Value item)
->返回与给定项目关联的密钥

6、Listing,用于UI显示列表和系统其余部分进行交互所必需的数据类

package per.lijuan.pagingdome.paging

import android.arch.lifecycle.LiveData
import android.arch.paging.PagedList

/**
 * UI显示列表和系统其余部分进行交互所必需的数据类
 * Created by juan on 2018/05/23.
 */
data class Listing<T> (
        val pagedList: LiveData<PagedList<T>>,
        val networkState: LiveData<Resource<String>>,
        val refreshState: LiveData<Resource<String>>,
        val refresh: () -> Unit,
        val retry: () -> Unit)

8、Resource,一个通用类,用于保存具有其加载状态的值

package per.lijuan.pagingdome.paging

/**
 *
 * 一个通用类,用于保存具有其加载状态的值
 * Created by juan on 2018/05/23.
 */
data class Resource<out T>(val status: Status, val data: T?, val message: String?) {
    companion object {
        fun <T> loading(msg: String? = null, data: T? = null): Resource<T> {
            return Resource(Status.LOADING, data, msg)
        }

        fun <T> success(data: T? = null): Resource<T> {
            return Resource(Status.SUCCESS, data, null)
        }

        fun <T> error(msg: String? = null, data: T? = null): Resource<T> {
            return Resource(Status.ERROR, data, msg)
        }
    }
}

enum class Status {
    SUCCESS,
    ERROR,
    LOADING
}

9、StudentViewModel
在Dome中我们是引用MVVM的应用框架,StudentViewModel只做和业务逻辑和业务数据相关的事,不做任何和UI相关的事情,负责完成View与Model间的交互。ViewModel 层不会持有任何控件的引用,更不会在ViewModel中通过UI控件的引用去做更新UI的事情。简单说就是专注于业务的逻辑处理,做的事情也都只是对数据的操作。后面我可能会专门写一篇MVVM的文章

package per.lijuan.pagingdome

import android.app.Application
import android.arch.lifecycle.AndroidViewModel
import android.arch.lifecycle.MutableLiveData
import android.arch.lifecycle.Transformations
import per.lijuan.pagingdome.paging.StudentRepository

/**
 * Created by juan on 2018/05/23.
 */
class StudentViewModel : AndroidViewModel {
    private lateinit var mPostRepository: StudentRepository

    constructor(application: Application, postRepository: StudentRepository):super(application){
        this.mPostRepository=postRepository
    }

    // region 基于Android官方Paging Library的分页加载框架
    private val data = MutableLiveData<String>()
    private val repoResult = Transformations.map(data, {
        mPostRepository.getStudentList(10)
    })

    val posts = Transformations.switchMap(repoResult, { it.pagedList })!!
    val networkState = Transformations.switchMap(repoResult, { it.networkState })!!
    val refreshState = Transformations.switchMap(repoResult, { it.refreshState })!!

    fun refresh() {
        repoResult.value?.refresh?.invoke()
    }

    fun showDatas(subreddit: String): Boolean {
        if (data.value == subreddit) {
            return false
        }
        data.value = subreddit
        return true
    }

    fun retry() {
        val listing = repoResult?.value
        listing?.retry?.invoke()
    }

    fun currentData(): String? = data.value

    // endregion
}

10、MainActivity,这里的Activity做的事就是初始化一些控件(如控件的颜色,添加RecyclerView的分割线等)、订阅Resource.Status的加载状态,从而更新UI。简单地说:View层不做任何业务逻辑、不涉及操作或处理数据。

package per.lijuan.pagingdome

import android.arch.lifecycle.Observer
import android.arch.lifecycle.ViewModel
import android.arch.lifecycle.ViewModelProvider
import android.arch.lifecycle.ViewModelProviders
import android.graphics.Color
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.support.v7.widget.DividerItemDecoration
import android.support.v7.widget.LinearLayoutManager
import android.widget.LinearLayout
import android.widget.Toast
import kotlinx.android.synthetic.main.activity_main.*
import per.lijuan.pagingdome.adapter.StudentAdapter
import per.lijuan.pagingdome.paging.ServiceLocator
import per.lijuan.pagingdome.paging.Status

class MainActivity : AppCompatActivity() {
    private lateinit var studentViewModel: StudentViewModel
    private lateinit var mAdapter: StudentAdapter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        studentViewModel=getViewModel()
        initAdapter()
        initSwipeToRefresh()
        studentViewModel.showDatas("")
    }

    private fun getViewModel(): StudentViewModel {
        return ViewModelProviders.of(this, object : ViewModelProvider.Factory {
            override fun <T : ViewModel?> create(modelClass: Class<T>): T {
                val repo = ServiceLocator.instance()
                        .getRepository()
                return StudentViewModel(application, repo) as T
            }
        })[StudentViewModel::class.java]
    }

    private fun initSwipeToRefresh() {
        swipeRefreshLayout.setColorSchemeColors(Color.BLUE, Color.GREEN, Color.RED, Color.YELLOW)
        swipeRefreshLayout.setOnRefreshListener {
            studentViewModel.refresh()
        }
        studentViewModel.refreshState.observe(this, Observer { resource->
            if (resource==null){
                return@Observer
            }
            when(resource.status){
                Status.LOADING->{
                    swipeRefreshLayout.isRefreshing=true
                }
                Status.SUCCESS->{
                    swipeRefreshLayout.isRefreshing=false
                }
                Status.ERROR->{
                    Toast.makeText(this,resource.message,Toast.LENGTH_LONG).show()
                    swipeRefreshLayout.isRefreshing=false
                }
            }
        })
    }

    private fun initAdapter() {
        val mLayoutManager = LinearLayoutManager(this)
        mLayoutManager.orientation = LinearLayout.VERTICAL
        rv!!.layoutManager = mLayoutManager
        rv.addItemDecoration(DividerItemDecoration(this, DividerItemDecoration.VERTICAL))//添加分割线

        mAdapter= StudentAdapter(this)
        rv.adapter = mAdapter

        studentViewModel.posts.observe(this, Observer((mAdapter::submitList)))
    }
}

其中studentViewModel.posts.observe(this, Observer((mAdapter::submitList)))就是把LiveData<PagedList>与PagedListAdapter关联连接。
11、StudentAdapter:

package per.lijuan.pagingdome.adapter

import android.arch.paging.PagedListAdapter
import android.content.Context
import android.support.v7.util.DiffUtil
import android.view.ViewGroup
import per.lijuan.pagingdome.paging.StudentBean

/**
 * Created by juan on 2018/05/23.
 */
class StudentAdapter(private val mContext: Context):PagedListAdapter<StudentBean, StudentViewHolder>(diffCallback){

    override fun onBindViewHolder(holder: StudentViewHolder, position: Int) {
        holder.bindTo(getItem(position),mContext)
    }

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): StudentViewHolder = StudentViewHolder(parent)

    companion object {
        private val diffCallback = object : DiffUtil.ItemCallback<StudentBean>() {
            override fun areItemsTheSame(oldItem: StudentBean, newItem: StudentBean): Boolean =
                    oldItem.id == newItem.id

            override fun areContentsTheSame(oldItem: StudentBean, newItem: StudentBean): Boolean =
                    oldItem == newItem
        }
    }
}

大概就这么多,我把现阶段对Android Paging Library学习过程记录下来,作为我学习Android Paging Library技术的阶段性备忘录,这代码还有待进一步完善和继续跟进研究!
有什么疑问的,请在下面留言,有不足之处还望指导,感谢各位_

源码下载

附录:
1、谷歌Android官方Android Paging Library技术文档主页:
https://developer.android.com/topic/libraries/architecture/paging/
2、Google samples :
https://github.com/googlesamples/android-architecture-components

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269