Paging Library是Google在2018年的io大会上发布的一个分页库,主要用于大量数据的分页加载,以减轻cpu负担,优化内存使用等。
其实之前一直可以自己实现这一功能,只不过这一次官方提供了标准化的api,对我这种只会Ctrl c + ctrl v的伪开发者来说无疑是个福音了。
之所以要用这个库,是因为当前的app中有一个我一直留到现在的bug,那就是新闻页面的评论加载不完全,只有第一页的最多30几个评论,后面的评论都被忽略了。
这是第一次使用kotlin和java混合开发,所以踩了不少坑。
Paging Library的使用其实也是挺简单的,主要是分成几步走:
一、改造原来的RecyclerView的Adaper
原先的Adapter直接派生自RecyclerView的内部抽象类Adapter,如果要应用于Paging Library,必须继承PagedListAdapter,从这个名字上就可以看出来,这个适配器是应用于“分成页加载的列表”,而这个PagedListAdapter又派生自RecyclerView.Adapter,中间多了一层继承关系,分页库在这里为我们封装好了一些必须的功能。
原来必须实现的getItemCount方法,在PagedListAdapter已经实现好了,改造的时候直接删除掉这个方法。
本来要在Activity中通过网络请求生成一个javaBean的列表,传进Adapter,Adapter才能根据这个列表进行model ->view的转换,而改造后的Adapter的数据直接从后面要提到的DataSource(数据源)中获取。
所以改造后,用于存放数据的list也可以直接删掉。
因为Paging Library一个核心点是根据需求进行动态加载,所以此时需要比较老的内容和新的内容是否重复,所以改造的时候需要增加一个DiffUtil.ItemCallback对象,用于确定新旧内容是否重复:
//用于确定是否重复
private static DiffUtil.ItemCallback<CommentBean> DIFF_CALLBACK = new DiffUtil.ItemCallback<CommentBean>() {
@Override
public boolean areItemsTheSame(@NonNull CommentBean oldItem, @NonNull CommentBean newItem) {
return oldItem.getCommentId() == newItem.getCommentId();
}
@Override
public boolean areContentsTheSame(@NonNull CommentBean oldItem, @NonNull CommentBean newItem) {
return oldItem.getCommentId() == newItem.getCommentId();
}
从这个匿名内部类对象的重写方法名字上可以看出,Paging Library确定是否重复是根据内容和整体来确定的,这里我只根据commentID来比较是否重复,所以两个方法都直接返回
oldItem.getCommentId() == newItem.getCommentId();
还有一个必须重写的getItemViewType方法,用于获取所要生成视图的类型:
@Override
public int getItemViewType(int position) {
return getItem(position).getViewType();
}
因为此时适配器的调用方从Activity变成了提供数据资源的DataSource对象,而且又删掉了保存JavaBean的列表,所以在改造后的适配器里面,直接用getItem(int position)方法就能获取所需要的数据,这个时候在onBindViewHolder方法里面,把list.get(position)替换为:
CommentBean comment = getItem(position);//从列表中拿出当前位置的数据
最后把适配器的构造函数改成:
public CommentAdapter(Activity context) {
super(DIFF_CALLBACK);
this.context = context;
}
利用父类的方法,比较是否重复,进而决定该内容是否加载到视图上面,不多说。
二、创建DataSource
DataSource(数据源)是分页库里面产生数据的地方,这个产生数据的过程本来是在Activity中完成的,现在全部都改到这里面来完成这个过程。
Paging Library提供的DataSource分成三类:
1.使用PageKeyDataSource,让你加载的页面插入到下一个或者以前的key,例如:例如:你要从网络获取社交媒体的帖子,你就需要通过nextPage加载到后续的加载中。
2.使用ItemKeyDataSource,如果你需要让使用的数据的item从N条增加到N+1条请使用。例如:你需要从嵌套评论中获取一条评论,需要通过一条评论的ID获取下一条评论的内容。
3.PositionalDataSource,如果你需要从事数据存储的任意位置来获取数据页,此类支持你从任意你选择的位置开始请求item的数据集。比如从第1200条返回20条item。
上面是我直接copy的官方文档,这里我采用的是positionalDataSource,编程语言使用的kotlin,新建一个kotlin类,并实现其中的两个抽象方法:
class CommentDataSource : PositionalDataSource<CommentBean>()
获取数据的方式,主要是通过网络连接,从云端获取数据(这个过程是直接从原来的ActivityBase里面抽出来的,只是被我重构了一下,从java语言变成了kotlin语言):
private fun requestComment(requestLink: String,loadType : Int,container : ArrayList<CommentBean>,callback: Any) {
//构造网络请求
val request = Request.Builder().url(requestLink)
.addHeader("Referer", articleUrl)
.addHeader("Accept", "application/json, text/javascript, */*; q=0.01")
.addHeader("Origin", "https://www.guancha.cn")
.addHeader("Accept-Language", "zh-CN,zh;q=0.9")
.get()
.build()
//监听,并处理返回
val call = client.newCall(request)
call.enqueue(object : Callback {
override fun onFailure(call: Call, e: IOException) {
throw SocketTimeoutException("连接超时!")
}
override fun onResponse(call: Call, response: Response) {
val respond = response.body?.string()
respond ?: return//返回的数据有问题就直接返回
//json转换过程也许会出现异常情况
try {
val responseInJson = JSONObject(respond)//封装成json对象
responseInJson.apply {
if (loadType == INITIAL_LOAD) {//提取全部的热评
val hotCount = getString("all_hot_count")
hotCommentsNum = hotCount.toInt()
if (hotCommentsNum != 0) {
parseComments(getJSONArray("hots"),
"热门评论 $hotCount 条",
loadType,
container)
}
}
//提取全部评论,每次网络请求返回的评论的数目都是20条,当不满足20条的时候,肯定是后面没有任何评论了
allCommentsNum = getString("count").toInt()
totalCommentsNum = when(hotCommentsNum){
0 -> when(allCommentsNum){
0 -> 0
else -> allCommentsNum +1
}
else -> hotCommentsNum + allCommentsNum + 2
}
if (allCommentsNum != 0) {
commentPages = allCommentsNum / 20 + 1
parseComments(getJSONArray("items"),
"所有评论 $allCommentsNum 条",
loadType,
container)
}
}
} catch (e: JSONException) {
e.printStackTrace()
}
if (loadType == INITIAL_LOAD){
(callback as LoadInitialCallback<CommentBean>).onResult(container,0)
}else if(loadType == RANGE_LOAD){
(callback as LoadRangeCallback<CommentBean>).onResult(container)
}
}
})
}
private fun parseComments(hotComments : JSONArray,headerName : String, loadType: Int,container : ArrayList<CommentBean>){
if (loadType == INITIAL_LOAD) {
val headerText = CommentBean(HEADER_VIEW,
0,
"",
"",
"",
"",
false,
"",
false,
"",false)
headerText.headerTitle = headerName
container.add(headerText)
}
for (i in 0 until hotComments.length()){
val hotBean = hotComments.getJSONObject(i)
val singleBean = hotBean.run {
CommentBean(COMMENT_VIEW,
getInt("id"),
getString("user_photo"),
getString("user_nick"),
getString("created_at"),
getString("content"),
getBoolean("has_praise"),
getInt("praise_num").toString(),
getBoolean("has_tread"),
getInt("tread_num").toString(),
getInt("parent_id") != 0)
}
if (singleBean.isParentExists){
val parentComments = hotBean.getJSONArray("parent").getJSONObject(0)
with(parentComments){
singleBean.run {
parentId = hotBean.getInt("parent_id")
parentUserName = getString("user_nick")
parentCommentTime = getString("created_at")
parentComment = getString("content")
isParentDisliked = getBoolean("has_tread")
parentDislikedNumber = getString("tread_num")
isParentUserPraised = getBoolean("has_praise")
parentPraisedNumber = getInt("praise_num").toString()
}
}
}
container.add(singleBean)
}
}
以上两个方法没什么好说的,主要还是用OKhttp框架来进行网络请求,并将网络响应结果里面的json数据封装成我们需要的列表。
派生自positionalDataSource,必须实现loadInitial和loadRange两个抽象方法,在这两个方法两个调用上面的网络请求,并返回给调用方:
override fun loadInitial(params: LoadInitialParams, callback: LoadInitialCallback<CommentBean>) {
//从数据库中取出codeID
commentList = ArrayList(40)
val codeId = LitePal.findLast(ArticleCodeId :: class.java)
codeId?: return
codeID = codeId.codeId
articleUrl = codeId.articleUrl
//构造请求链接
val requestLink = "https://user.guancha.cn/comment/cmt-list.json?codeId=$codeID&codeType=1&pageNo=$pageIndex&order=1&ff=www"
pageIndex++//页数自增,为下次动态增加做准备
requestComment(requestLink,INITIAL_LOAD,commentList,callback)
Log.d(TAG,"此时LoadInitial方法已经返回")
}
override fun loadRange(params: LoadRangeParams, callback: LoadRangeCallback<CommentBean>) {
Log.d(TAG,"loadRange方法被调用")
val commentContainer = ArrayList<CommentBean>(20)
if (pageIndex> commentPages) return
val nextPageUrl = "https://user.guancha.cn/comment/cmt-list.json?codeId=$codeID&codeType=1&pageNo=$pageIndex&order=1&ff=www"
pageIndex ++
requestComment(nextPageUrl,RANGE_LOAD,commentContainer,callback)
}
其中loadInitial方法,是第一次加载数据的时候会被调用的方法,loadRange是之后的每一页数据加载的时候调用的方法。
三、创建Datasource工厂类
DataSource工厂类,主要用于生成DataSource,然后和ViewModel(视图模型)进行绑定:
class CommentFactory : DataSource.Factory<Int,CommentBean>(){
private val sourceMutableLiveData = MutableLiveData<CommentDataSource>()
override fun create(): DataSource<Int, CommentBean> {
val dataSource = CommentDataSource()
sourceMutableLiveData.postValue(dataSource)
return dataSource
}
}
代码很简单,就是创建一个MutableLiveData对象,然后用这个LiveData对象处理一下DataSource,然后把DataSource返回。
四、创建ViewModel
ViewModel的作用,主要是管理DataSource,掌控好时机进行数据的加载:
class CommentViewModel : ViewModel()
涉及到IO操作的,一般会在后台线程中进行,所以首先创建一个单线程的线程池:
private val executor = Executors.newSingleThreadExecutor()
然后需要配置分页的参数,包括初始加载多少,分页是多大,是否需要占位视图等等:
private val config = PagedList.Config.Builder()
.setInitialLoadSizeHint(20)
.setPageSize(20)
.setPrefetchDistance(5)
.setEnablePlaceholders(false)
.build()
然后就是把上面的配置参数,datasource,livedata,还有数据列表都结合起来:
private val sourceDataFactory : DataSource.Factory<Int,CommentBean> = CommentFactory()
val newsList : LiveData<PagedList<CommentBean>> = LivePagedListBuilder(sourceDataFactory,config)
.setFetchExecutor(executor).build()
fun invalidateDataSource(){
val pagedList = newsList.value
pagedList?.dataSource?.invalidate()
}
五:修改Activity:
这一步很简单,因为最终把控整个流程的是ViewModel,所以在Activity创建一个ViewModel对象,并用RxJava风格的代码让这个ViewModel以观察者模式监视整个列表的运行状况:
ViewModelProvider provider = new ViewModelProvider(getViewModelStore(),new ViewModelProvider.AndroidViewModelFactory(Objects.requireNonNull(getApplication())));
commentViewModel = provider.get(CommentViewModel.class);
commentViewModel.getNewsList().observe(this, commentBeans -> adapter.submitList(commentViewModel.getNewsList().getValue()));
然后,就能看到,所有的评论都被加载了: