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

匿名 (未验证) 提交于 2019-12-03 00:19:01

在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.google.cn/topic/libraries/architecture/paging/
我写这篇文章是基于Paging Library 1.0.0的版本。

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

    //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、定义一个StudentDataRepository,实现了下面定义的StudentRepository接口,实现返回一个直接从网络加载数据的Listing,并使用该名称作为加载上一页/下一页数据的关键。在这里我们可以定义从DataSource中第一页以及后面每一页加载的项目数量等。

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  /**  * Repository实现返回一个直接从网络加载数据的Listing,并使用该名称作为加载上一页/下一页数据的关键  * 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)     } }

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

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

4、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 params,
@NonNull LoadInitialCallback callback)
->用于接收初始第一页加载的数据,在这里需要将获取到的数据通过LoadInitialCallback的onResult进行回调,用于出始化PagedList,并对加载的项目进行计数

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

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

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

5、StudentDataSourceFactory,一个简单的数据源工厂,它提供了一种观察上次创建的数据源的方式,这使得我们能够将其网络请求状态等返回到UI界面

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     } }

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)))     } }

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

源码下载

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

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