谷歌分页库Paging Library 的应用

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()));

然后,就能看到,所有的评论都被加载了:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值