6. Jetpack---Paging你知道怎样上拉加载吗?
之前的几篇源码分析我们分别对
Navigation
、Lifecycles
、ViewModel
、LiveData
、进行了分析,也对JetPack有了更深入的了解。但是Jetpack远不止这些组件,今天的主角—Paging,Jetpack中的分页组件,官方是这么形容它的:‘’逐步从您的数据源按需加载信息‘’
如果你对Jetpack组件有了解或者想对源码有更深入的了解,请看我之前的几篇文章:
1. Jetpack源码解析—看完你就知道Navigation是什么了?
2. Jetpack源码解析—Navigation为什么切换Fragment会重绘?
3. Jetpack源码解析—用Lifecycles管理生命周期
4. Jetpack源码解析—LiveData的使用及工作原理
5. Jetpack源码解析—ViewModel基本使用及源码解析
1. 背景
在我的Jetpack_Note系列中,对每一篇的分析都有相对应的代码片段及使用,我把它做成了一个APP,目前功能还不完善,代码我也上传到了GitHub上,参考了官方的Demo以及目前网上的一些文章,有兴趣的小伙伴可以看一下,别忘了给个Star。
https://github.com/Hankkin/JetPack_Note
今天我们的主角是Paging,介绍之前我们先看一下效果:
2. 简介
2.1 基本介绍
官方定义:
分页库Pagin Library是Jetpack的一部分,它可以妥善的逐步加载数据,帮助您一次加载和显示一部分数据,这样的按需加载可以减少网络贷款和系统资源的使用。分页库支持加载有限以及无限的list,比如一个持续更新的信息源,分页库可以与RecycleView无缝集合,它还可以与LiveData或RxJava集成,观察界面中的数据变化。
2.2 核心组件
1. PagedList
PageList是一个集合类,它以分块的形式异步加载数据,每一块我们称之为页。它继承自AbstractList
,支持所有List的操作,它的内部有五个主要变量:
- mMainThreadExecutor 主线程Executor,用于将结果传递到主线程
- mBackgroundThreadExecutor 后台线程,执行负载业务逻辑
- BoundaryCallback 当界面显示缓存中靠近结尾的数据的时候,它将加载更多的数据
- Config PageList从DataSource中加载数据的配置
- PagedStorage 用于存储加载到的数据
Config属性:
- pageSize:分页加载的数量
- prefetchDistance:预加载的数量
- initialLoadSizeHint:初始化数据时加载的数量,默认为pageSize*3
- enablePlaceholders:当item为null是否使用placeholder显示
PageList会通过DataSource加载数据,通过Config的配置,可以设置一次加载的数量以及预加载的数量。除此之外,PageList还可以想RecycleView.Adapter发送更新的信号,驱动UI的刷新。
2. DataSource
DataSource<Key,Value> 顾名思义就是数据源,它是一个抽象类,其中Key
对应加载数据的条件信息,Value
对应加载数据的实体类。Paging库中提供了三个子类来让我们在不同场景的情况下使用:
- PageKeyedDataSource:如果后端API返回数据是分页之后的,可以使用它;例如:官方Demo中GitHub API中的SearchRespositories就可以返回分页数据,我们在GitHub API的请求中制定查询关键字和想要的哪一页,同时也可以指明每个页面的项数。
- ItemKeyedDataSource:如果通过键值请求后端数据;例如我们需要获取在某个特定日期起Github的前100项代码提交记录,该日期将成为DataSource的键,ItemKeyedDataSource允许自定义如何加载初始页;该场景多用于评论信息等类似请求
- PositionalDataSource:适用于目标数据总数固定,通过特定的位置加载数据,这里Key是Integer类型的位置信息,T即Value。 比如从数据库中的1200条开始加在20条数据。
3. PagedListAdapter
PageListAdapter继承自RecycleView.Adapter,和RecycleView实现方式一样,当数据加载完毕时,通知RecycleView数据加载完毕,RecycleView填充数据;当数据发生变化时,PageListAdapter会接受到通知,交给委托类AsyncPagedListDiffer来处理,AsyncPagedListDiffer是对**DiffUtil.ItemCallback**持有对象的委托类,AsyncPagedListDiffer使用后台线程来计算PagedList的改变,item是否改变,由DiffUtil.ItemCallback决定。
3.基本使用
3.1 添加依赖包
implementation "androidx.paging:paging-runtime:$paging_version" // For Kotlin use paging-runtime-ktximplementation "androidx.paging:paging-runtime-ktx:$paging_version" // For Kotlin use paging-runtime-ktx// alternatively - without Android dependencies for testingtestImplementation "androidx.paging:paging-common:$paging_version" // For Kotlin use paging-common-ktx// optional - RxJava supportimplementation "androidx.paging:paging-rxjava2:$paging_version" // For Kotlin use paging-rxjava2-ktx
3.2 PagingWithRoom使用
新建UserDao
/*** created by Hankkin* on 2019-07-19*/
@Dao
interface UserDao {@Query("SELECT * FROM User ORDER BY name COLLATE NOCASE ASC")fun queryUsersByName(): DataSource.Factory<Int, User>@Insertfun insert(users: List<User>)@Insertfun insert(user: User)@Deletefun delete(user: User)}
创建UserDB数据库
/*** created by Hankkin* on 2019-07-19*/
@Database(entities = arrayOf(User::class), version = 1)
abstract class UserDB : RoomDatabase() {abstract fun userDao(): UserDaocompanion object {private var instance: UserDB? = null@Synchronizedfun get(context: Context): UserDB {if (instance == null) {instance = Room.databaseBuilder(context.applicationContext,UserDB::class.java, "UserDatabase").addCallback(object : RoomDatabase.Callback() {override fun onCreate(db: SupportSQLiteDatabase) {fillInDb(context.applicationContext)}}).build()}return instance!!}/*** fill database with list of cheeses*/private fun fillInDb(context: Context) {// inserts in Room are executed on the current thread, so we insert in the backgroundioThread {get(context).userDao().insert(CHEESE_DATA.map { User(id = 0, name = it) })}}}
}
创建PageListAdapter
/*** created by Hankkin* on 2019-07-19*/
class PagingDemoAdapter : PagedListAdapter<User, PagingDemoAdapter.ViewHolder>(diffCallback) {override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =ViewHolder(AdapterPagingItemBinding.inflate(LayoutInflater.from(parent.context), parent, false))override fun onBindViewHolder(holder: ViewHolder, position: Int) {val item = getItem(position)holder.apply {bind(createOnClickListener(item), item)itemView.tag = item}}private fun createOnClickListener(item: User?): View.OnClickListener {return View.OnClickListener {Toast.makeText(it.context, item?.name, Toast.LENGTH_SHORT).show()}}class ViewHolder(private val binding: AdapterPagingItemBinding) : RecyclerView.ViewHolder(binding.root) {fun bind(listener: View.OnClickListener, item: User?) {binding.apply {clickListener = listeneruser = itemexecutePendingBindings()}}}companion object {/*** This diff callback informs the PagedListAdapter how to compute list differences when new* PagedLists arrive.* <p>* When you add a Cheese with the 'Add' button, the PagedListAdapter uses diffCallback to* detect there's only a single item difference from before, so it only needs to animate and* rebind a single view.** @see android.support.v7.util.DiffUtil*/private val diffCallback = object : DiffUtil.ItemCallback<User>() {override fun areItemsTheSame(oldItem: User, newItem: User): Boolean =oldItem.id == newItem.id/*** Note that in kotlin, == checking on data classes compares all contents, but in Java,* typically you'll implement Object#equals, and use it to compare object contents.*/override fun areContentsTheSame(oldItem: User, newItem: User): Boolean =oldItem == newItem}}
}
ViewModel承载数据
class PagingWithDaoViewModel internal constructor(private val pagingRespository: PagingRespository) : ViewModel() {val allUsers = pagingRespository.getAllUsers()fun insert(text: CharSequence) {pagingRespository.insert(text)}fun remove(user: User) {pagingRespository.remove(user)}
}
Activity中观察到数据源的变化后,会通知Adapter自动更新数据
class PagingWithDaoActivity : AppCompatActivity() {private lateinit var viewModel: PagingWithDaoViewModeloverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_paging_with_dao)setLightMode()setupToolBar(toolbar) {title = resources.getString(R.string.paging_with_dao)setDisplayHomeAsUpEnabled(true)}viewModel = obtainViewModel(PagingWithDaoViewModel::class.java)val adapter = PagingDemoAdapter()rv_paging.adapter = adapterviewModel.allUsers.observe(this, Observer(adapter::submitList))}override fun onOptionsItemSelected(item: MenuItem?): Boolean {when (item?.itemId) {android.R.id.home -> finish()}return super.onOptionsItemSelected(item)}
}
3.3 PagingWithNetWork 使用
上面我们通过Room进行了数据库加载数据,下面看一下通过网络请求记载列表数据:
和上面不同的就是Respository数据源的加载,之前我们是通过Room加载DB数据,现在我们要通过网络获取数据:
GankRespository 干货数据源仓库
/*** created by Hankkin* on 2019-07-30*/
class GankRespository {companion object {private const val PAGE_SIZE = 20@Volatileprivate var instance: GankRespository? = nullfun getInstance() =instance ?: synchronized(this) {instance?: GankRespository().also { instance = it }}}fun getGank(): Listing<Gank> {val sourceFactory = GankSourceFactory()val config = PagedList.Config.Builder().setPageSize(PAGE_SIZE).setInitialLoadSizeHint(PAGE_SIZE * 2).setEnablePlaceholders(false).build()val livePageList = LivePagedListBuilder<Int, Gank>(sourceFactory, config).build()val refreshState = Transformations.switchMap(sourceFactory.sourceLiveData) { it.initialLoad }return Listing(pagedList = livePageList,networkState = Transformations.switchMap(sourceFactory.sourceLiveData) { it.netWorkState },retry = { sourceFactory.sourceLiveData.value?.retryAllFailed() },refresh = { sourceFactory.sourceLiveData.value?.invalidate() },refreshState = refreshState)}}
可以看到getGank()方法返回了Listing,那么Listing
是个什么呢?
/*** Data class that is necessary for a UI to show a listing and interact w/ the rest of the system* 封装需要监听的对象和执行的操作,用于上拉下拉操作* pagedList : 数据列表* networkState : 网络状态* refreshState : 刷新状态* refresh : 刷新操作* retry : 重试操作*/
data class Listing<T>(// the LiveData of paged lists for the UI to observeval pagedList: LiveData<PagedList<T>>,// represents the network request status to show to the userval networkState: LiveData<NetworkState>,// represents the refresh status to show to the user. Separate from networkState, this// value is importantly only when refresh is requested.val refreshState: LiveData<NetworkState>,// refreshes the whole data and fetches it from scratch.val refresh: () -> Unit,// retries any failed requests.val retry: () -> Unit)
Listing是我们封装的一个数据类,将数据源、网络状态、刷新状态、下拉刷新操作以及重试操作都封装进去了。那么我们的数据源从哪里获取呢,可以看到Listing的第一个参数pageList = livePageList
,livePageList
通过LivePagedListBuilder创建,LivePagedListBuilder需要两个参数(DataSource
,PagedList.Config
):
GankSourceFactory
/*** created by Hankkin* on 2019-07-30*/
class GankSourceFactory(private val api: Api = Injection.provideApi()) : DataSource.Factory<Int, Gank>(){val sourceLiveData = MutableLiveData<GankDataSource>()override fun create(): DataSource<Int, Gank> {val source = GankDataSource(api)sourceLiveData.postValue(source)return source}
}
GankDataSource
/*** created by Hankkin* on 2019-07-30*/
class GankDataSource(private val api: Api = Injection.provideApi()) : PageKeyedDataSource<Int, Gank>() {private var retry: (() -> Any)? = nullval netWorkState = MutableLiveData<NetworkState>()val initialLoad = MutableLiveData<NetworkState>()fun retryAllFailed() {val prevRetry = retryretry = nullprevRetry?.also { it.invoke() }}override fun loadInitial(params: LoadInitialParams<Int>, callback: LoadInitialCallback<Int, Gank>) {initialLoad.postValue(NetworkState.LOADED)netWorkState.postValue(NetworkState.HIDDEN)api.getGank(params.requestedLoadSize, 1).enqueue(object : Callback<GankResponse> {override fun onFailure(call: Call<GankResponse>, t: Throwable) {retry = {loadInitial(params, callback)}initialLoad.postValue(NetworkState.FAILED)}override fun onResponse(call: Call<GankResponse>, response: Response<GankResponse>) {if (response.isSuccessful) {retry = nullcallback.onResult(response.body()?.results ?: emptyList(),null,2)initialLoad.postValue(NetworkState.LOADED)} else {retry = {loadInitial(params, callback)}initialLoad.postValue(NetworkState.FAILED)}}})}override fun loadAfter(params: LoadParams<Int>, callback: LoadCallback<Int, Gank>) {netWorkState.postValue(NetworkState.LOADING)api.getGank(params.requestedLoadSize, params.key).enqueue(object : Callback<GankResponse> {override fun onFailure(call: Call<GankResponse>, t: Throwable) {retry = {loadAfter(params, callback)}netWorkState.postValue(NetworkState.FAILED)}override fun onResponse(call: Call<GankResponse>, response: Response<GankResponse>) {if (response.isSuccessful) {retry = nullcallback.onResult(response.body()?.results ?: emptyList(),params.key + 1)netWorkState.postValue(NetworkState.LOADED)} else {retry = {loadAfter(params, callback)}netWorkState.postValue(NetworkState.FAILED)}}})}override fun loadBefore(params: LoadParams<Int>, callback: LoadCallback<Int, Gank>) {}}
网络请求的核心代码在GankDataSource中,因为我们的请求是分页请求,所以这里的GankDataSource
我们继承自PageKeyedDataSource
,它实现了三个方法:
loadInitial
: 初始化加载,初始加载的数据 也就是我们直接能看见的数据
loadAfter
: 下一页加载,每次传递的第二个参数 就是 你加载数据依赖的key
loadBefore
: 往上滑加载的数据
可以看到我们在loadInitial
中设置了initialLoad
和netWorkState
的状态值,同时通过RetrofitApi获取网络数据,并在成功和失败的回调中对数据和网络状态值以及加载初始化做了相关的设置,具体就不介绍了,可看代码。loadAfter
同理,只不过我们在加载数据后对key也就是我们的page进行了+1操作。
Config参数就是我们对分页加载的一些配置:
val config = PagedList.Config.Builder().setPageSize(PAGE_SIZE).setInitialLoadSizeHint(PAGE_SIZE * 2).setEnablePlaceholders(false).build()
下面看我们在Activity中怎样使用:
PagingWithNetWorkActivity
class PagingWithNetWorkActivity : AppCompatActivity() {private lateinit var mViewModel: PagingWithNetWorkViewModelprivate lateinit var mDataBinding: ActivityPagingWithNetWorkBindingoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)mDataBinding = DataBindingUtil.setContentView(this,R.layout.activity_paging_with_net_work)setLightMode()setupToolBar(toolbar) {title = resources.getString(R.string.paging_with_network)setDisplayHomeAsUpEnabled(true)}mViewModel = obtainViewModel(PagingWithNetWorkViewModel::class.java)mDataBinding.vm = mViewModelmDataBinding.lifecycleOwner = thisval adapter = PagingWithNetWorkAdapter()mDataBinding.rvPagingWithNetwork.adapter = adaptermDataBinding.vm?.gankList?.observe(this, Observer { adapter.submitList(it) })mDataBinding.vm?.refreshState?.observe(this, Observer {mDataBinding.rvPagingWithNetwork.post {mDataBinding.swipeRefresh.isRefreshing = it == NetworkState.LOADING}})mDataBinding.vm?.netWorkState?.observe(this, Observer {adapter.setNetworkState(it)})}override fun onOptionsItemSelected(item: MenuItem?): Boolean {when (item?.itemId) {android.R.id.home -> finish()}return super.onOptionsItemSelected(item)}
}
ViewModel
中的gankList
是一个LiveData
,所以我们在这里给它设置一个观察,当数据变动是调用adapter.submitList(it)
,刷新数据,这个方法是PagedListAdapter中的,里面回去检查新数据和旧数据是否相同,也就是上面我们提到的AsyncPagedListDiffer
来实现的。到这里整个流程就已经结束了,想看源码可以到Github上。
4. 总结
我们先看下官网给出的gif图:
[外链图片转存失败(img-eFq85sdR-1566831114700)(https://note.youdao.com/yws/api/personal/file/WEBd1ac1c87130f18afd376a4f7fb273bb0?method=download&shareKey=460a039c8e8695464d321519258a104b)]
总结一下,Paging的基本原理为:
- 使用DataSource从网络或者数据库获取数据
- 将数据保存到PageList中
- 将PageList中的数据提交给PageListAdapter
- PageListAdapter在后台线程中通过Diff对比新老数据,反馈到RecycleView中
- RecycleView刷新数据
基本原理在图上我们可以很清晰的了解到了,本篇文章的Demo中结合了ViewModel以及DataBinding进行了数据的存储和绑定。
最后代码地址:
https://github.com/Hankkin/JetPack_Note
6. Jetpack---Paging你知道怎样上拉加载吗?相关推荐
- vue.js上拉加载
vue.js上拉加载 注意事项: 1.overflow属性会导致滚动事件失效 2.连续下拉会导致数据加载时出现问题,给了1s的延迟 <!DOCTYPE html> <html lan ...
- 【微信小程序】实现上拉加载更多
小程序上拉加载更多 首先我们得运用到小程序的往下拉触底事件,如下图所示 代码量很少,方便理解,主要还是要有逻辑,也不说废话了直接上代码吧 data: {paging:5//显示几条内容}onReach ...
- uniapp 上拉加载更多完整实现源码
直接上代码 <template><view class="searchList"><!-- 搜索框 --><Search></ ...
- 20-flutter下拉刷新与上拉加载
1 RefreshIndicator 下拉刷新控件 下拉刷新的时候会回调 onRefresh 方法 RefreshIndicator(onRefresh: _handleRefresh,child: ...
- 小程序一次性上传多个本地图片,上拉加载照片以及图片加载延迟解决之道
一:小程序之一次性上传多个本地相片 最近由于项目需要所以学了下小程序,也做了一些东西,随后便有了以下的一些总结了,现在说说如何使用小程序一次性上传多个本地相片. 问题描述 最近做项目的时候要实现一个上 ...
- 安卓下拉刷新、上拉加载数据显示
整个是一个scrollView,嵌套一个线性布局,下拉刷新.或者上拉加载后,通过addView()方法,加载消息体,每一个消息体是一个复杂的子view. 做一个类似qq客户端"好友动态&qu ...
- mpvue 小程序如何开启下拉刷新,上拉加载?
https://developers.weixin.qq.com/miniprogram/dev/api/pulldown.html#onpulldownrefresh 小程序API 微信小程序之下拉 ...
- 移动端web页面列表类上拉加载,查看详情,iframe嵌套第三方页面遇到的问题以及解决办法...
1.移动端上拉加载 网上有很多成熟的插件,比如iscroll.在这里介绍一下用jquery和js写的上拉加载方法.使用原生的去写上拉加载更多需要三个高度去做对比,以新闻类列表举例,首先需要整个dom的 ...
- 通过Scroller.js制作上拉加载和下拉刷新
为什么80%的码农都做不了架构师?>>> 之前做移动端webAPP开发,一直是用的IScroll来做滚动列表,但是IScroll没有直接提供上下拉刷新的功能,虽然我们基于ISc ...
- 上拉加载下拉刷新了解下
2019独角兽企业重金招聘Python工程师标准>>> 老样子,我们先,哦不,今天我们直接上思路,没有效果图,真的没有 我们依旧从界面及逻辑两块进行分析 1.界面上,只分成简单的两块 ...
最新文章
- Redis大集群扩容性能优化实践
- Nature Microbiology: 微生物数据的系统发育分析方法
- Struts2后期(这框架目前正处于淘汰状态)
- ORA-01504问题
- 不是语言之争---Go vs Erlang
- 仿写thinkphp的I方法
- 小程序离成功还差一个版本
- php 内置mail 包,配置php自带的mail功能
- Python3实现打家劫舍问题
- makefile编译问题记录
- Android中的AutoCompleteTextView组件
- 又一门国产数据库语言诞生了,比SQL还好用
- 学习设计模式 - 六大基本原则之开闭原则
- SpringBoot 启动过程,你不知道的秘密!
- 【优化分类】基于matlab改进的人工蜂群算法优化SVM分类【含Matlab源码 1833期】
- 数学建模--因子分析模型
- matlab画柱状图并填充
- 【视频目标检测数据集收集】B站、YouTube等各大网站视频下载工具:Annie(现更名为lux)的下载与安装教程
- Orange:一个基于 Python 的数据挖掘和机器学习平台
- 聊聊数据治理与成本管理
热门文章
- 超市收银系统c语言,C语言超市收银系统.docx
- 通俗易懂介绍一下ZigBee的特性
- Java中的除法结果与除数被除数的类型有关
- 华为云,如何带领企业成功上云?
- Hyperledger(超级账本)
- Ant Design Pro项目启动报错 ChunkError mf-va_remoteEntry umi
- 四十三、文件传输协议FTP、电子邮件、万维网
- FTP(File Transfer Protocol,文件传输协议)
- Python的开源人脸识别库:离线识别率高达99.38%【源码】
- 我整理了十套 SpringBoot 项目完整教程「源码+视频+讲义」