欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  移动技术

Kotlin实战案例:带你实现RecyclerView分页查询功能(仿照主流电商APP,可切换列表和网格效果)

程序员文章站 2022-06-23 22:39:00
随着Kotlin的推广,一些国内公司的安卓项目开发,已经从Java完全切成Kotlin了。虽然Kotlin在各类编程语言中的排名比较靠后(据TIOBE发布了 19 年 8 月份的编程语言排行榜,Kotlin竟然排名45位),但是作为安卓开发者,掌握该语言,却已是大势所趋了。 Kotlin的基础用法, ......

Kotlin实战案例:带你实现RecyclerView分页查询功能(仿照主流电商APP,可切换列表和网格效果)

Kotlin实战案例:带你实现RecyclerView分页查询功能(仿照主流电商APP,可切换列表和网格效果)

随着kotlin的推广,一些国内公司的安卓项目开发,已经从java完全切成kotlin了。虽然kotlin在各类编程语言中的排名比较靠后(据tiobe发布了 19 年 8 月份的编程语言排行榜,kotlin竟然排名45位),但是作为安卓开发者,掌握该语言,却已是大势所趋了。

kotlin的基础用法,整体还是比较简单的,网上已经有很多文章了,大家熟悉下即可。

案例需求

此次案例,之所以选择分页列表,主要是因为该功能通用性强,涵盖的技术点也较多,对开发者熟悉kotlin帮助性较大。

案例的主要需求如下( 参考主流电商app实现 ):
1、列表支持手势滑动分页查询(滑动到底部时,自动查询下一页,直到没有更多数据)
2、可切换列表样式和网格样式
3、切换样式后,数据位置保持不变(如当前在第100条位置,切换样式后,位置不变)
4、footview根据查询状态,显示不同内容:

a、数据加载中... (正在查询数据时显示)
b、没有更多数据了 (查询成功,但是已没有可返回的数据了)
c、出错了,点击重试!!(查询时出现异常,可能是网络,也可能是其他原因)

5、当查询出错时,再次点击footview,可重新发起请求(例如:网络异常了)
6、当切换网格样式时,footview应独占一行

设计

虽然是简单案例,咱们开发时,也应先进行简单的设计,让各模块、各类都各司其职、逻辑解耦,这样大家学起来会更简单一些。
此处,不画类图了,直接根据项目结构,简单介绍一下吧:

Kotlin实战案例:带你实现RecyclerView分页查询功能(仿照主流电商APP,可切换列表和网格效果)

1、pagedata 是指数据模块,包含:

datainfo 实体类,定义商品字段属性
datasearch 数据访问类,模拟子线程异步查询分页数据,可将数据结果通过lambda回调回去

2、pagemangage 是指分页管理模块,将分页的全部逻辑托管给该模块处理。为了简化分页逻辑的实现,根据功能单一性进行了简单拆分:

pagesmanager 分页管理类,用于统筹列表数据查询、展示、样式切换
pageslayoutmanager 分页布局管理类,用于列表样式和网格样式的管理、数据位置记录
pagesdatamanager 分页数据管理类,用于分页列表数据、footview数据的封装

3、adapter 是指适配器模块,主要用于定义各类适配器

pagesadapter 分页适配器类,用于创建、展示 itemview、footview,footview回调事件

4、utils 是指工具模块,用于定义一些常用工具

apputils 工具类,用于判断网络连接情况

代码实现

在文章的最后,会将demo源码下载地址分享给大家,以供参考。

1、pagedata 数据模块

1.1、datainfo.kt 实体类

定义了属性,kotlin已包含属性默认的get、set

package com.qxc.kotlinpages.pagedata

/**
 * 实体类
 *
 * @author 齐行超
 * @date 19.11.30
 */
class datainfo {
    /**
     * 标题
     */
    var title: string = ""
    /**
     * 描述
     */
    var desc: string = ""
    /**
     * 图片
     */
    var imageurl: string = ""
    /**
     * 价格
     */
    var price: string = ""
    /**
     * 链接
     */
    var link: string = ""
}
1.2、datasearch 数据访问类:
package com.qxc.kotlinpages.pagedata
import com.qxc.kotlinpages.utils.apputils

/**
 * 数据查询类:模拟网络请求,从服务器数据库获取数据
 *
 * @author 齐行超
 * @date 19.11.30
 */
class datasearch {
    //服务器数据库中的数据总数量(模拟)
    private val totalnum = 25

    //声明回调函数(lambda表达式参数:errorcode错误码,datainfos数据,无返回值)
    private lateinit var listener: (errorcode: int, datainfos: arraylist<datainfo>) -> unit

    /**
     * 设置数据请求监听器
     *
     * @param plistener 数据请求监听器的回调类对象
     */
    fun setdatarequestlistener(plistener: 
                              (errorcode: int, datainfos: arraylist<datainfo>) -> unit) {
        this.listener = plistener
    }

    /**
     * 查询方法(模拟从数据库查询)
     * positionnum: 数据起始位置
     * pagenum: 查询数量(每页查询数量)
     */
    fun search(positionnum: int, pagenum: int) {
        //模拟网络异步请求(子线程中,进行异步请求)
        thread()
        {
            //模拟网络查询耗时
            thread.sleep(1000)

            //获得数据查询结束位置
            var end: int = if (positionnum + pagenum > totalnum) totalnum 
                           else positionnum + pagenum
            //创建集合
            var datas = arraylist<datainfo>()

            //判断网络状态
            if (!apputils.instance.isconnectnetwork()) {
                //回调异常结果
                this.listener(1,datas)
                //回调异常结果
                //this.listener.invoke(-1, datas)
                return@thread
            }

            //组织数据(模拟获取到数据)
            for (index in positionnum..end - 1) {
                var data = datainfo()
                data.title = "android kotlin ${index + 1}"
                data.desc = "kotlin 是一个用于现代多平台应用的静态编程语言,由 jetbrains ..."
                data.price = (100 * (index + 1)).tostring()
                data.imageurl = ""
                data.link = "跳转至kotlin柜台 -> jetbrains"
                datas.add(data)
            }

            //回调结果
            this.listener.invoke(0, datas)

        }.start()
    }
}

datasearch类有两个重点知识:

a、子线程异步查询的实现

a、参考常规分页网络请求api,调用方应传参:起始位置、每页数量
b、网络查询,需要在子线程中操作,当前案例直接使用thread{}.start()实现子线程


-----------------------------------thread{}.start()-------------------------------------
通常情况下,java中实现一个线程,可这么写:
new thread(new runnable() {
            @override
            public void run() {

            }
        }).start();

如果完全按照java的写法,将其转为kotlin,应该这么写:
thread(object: runnable{
            override fun run() {

            }
        }).start()

但是在本案例中却是:thread{}.start(),并未看到runnable对象和run方法,其实是被lambda表达式替换了:
>>  runnable接口有且仅有1个抽象函数run(),符合“函数式接口”定义(即:一个接口仅有一个抽象方法) 
>>  这样的接口可以使用lambda表达式来简化代码的编写 :

使用lambda表示runnable接口实现,因run()无参数、无返回值,对应的lambda实现结构应该是:
 { -> } 

当lambda表达式无任何参数时,可以省略箭头符号:
 { } 

我们将lambda表达式替换runnable接口实现,kotlin代码如下所示:
thread({ }) .start()

如果lambda表达式是函数是最后一个实参,则可以放在小括号后面:
thread() { }  .start()

如果lambda是函数的唯一实参的时候,小括号可以直接省略,也就变成了咱们案例的效果了:
thread{ } .start()

-----------------------------------thread{}.start()-------------------------------------

以上是线程lambda表达式的简化过程!!!
使用lambda表达式,使得我们可以在 “thread{ }” 的大括号里直接写子线程实现,代码变简单了

更多lambda表达式介绍,请参考文章:https://www.cnblogs.com/jetictors/p/8647888.html

b、数据回调监听

此案例通过lambda表达式实现了数据回调监听(与ios的block类似):

a、声明lambda表达式,用于定义回调方法结构(参数、返回值),如:
      var listener: (errorcode: int, datainfos: arraylist<datainfo>) -> unit
      lambda表达式可理解为一种特殊类型,即:抽象方法类型

b、由调用方传递过来lambda表达式的实参对象(即:调用方已实现的lambda表达式所表示的抽象方法)
      setdatarequestlistener(plistener:....)

c、当执行完数据查询时,将结果通过调用lambda表达式实参对象回传回去,如:
      this.listener(1,datas)
      this.listener.invoke(0, datas)
      这两种调用方式都是可以的,invoke是指执行自身


当然,我们也可以在kotlin中使用接口回调的那种方式,与java一样,只是代码会繁琐一些而已!!!

2、pagemangage 分页管理模块

为了简化分页逻辑,让大家更好理解,此处将分页数据、分页布局拆分出来,使其逻辑解耦,也便于代码的管理维护。

2.1、pagesdatamanager 分页数据管理类

主要包含内容:

1、配置分页数据的查询位置、每页数量,每次查询数据后重新计算下次查询位置
2、分页数据接口查询
3、分页状态文本处理(数据查询中、没有更多数据了、查询异常...)
package com.qxc.kotlinpages.pagemanage

import android.os.handler
import android.os.looper
import android.util.log
import com.qxc.kotlinpages.pagedata.datainfo
import com.qxc.kotlinpages.pagedata.datasearch

/**
 * 分页数据管理类:
 * 1、分页数据的查询位置、每页数量
 * 2、分页数据接口查询
 * 3、分页状态文本处理
 *
 * @author 齐行超
 * @date 19.11.30
 */
class pagesdatamanager {
    var startindex = 0 //分页起始位置
    val pagenum = 10 //每页数量
    val type_page_more = "数据加载中..." //分页加载类型:更多数据
    val type_page_last = "没有更多数据了" //分页加载类型:没有数据了
    val type_page_error = "出错了,点击重试!!" //分页加载类型:出错了,当这种状态时可点击重试

    //定义数据回调监听
    //参数:datainfos 当前页数据集合, foottype 分页状态文本
    lateinit var listener: ((datainfos: arraylist<datainfo>, foottype: string) -> unit)

    /**
     * 设置回调
     */
    fun setdatalistener(plistener: 
                       (datainfos: arraylist<datainfo>, foottype: string) -> unit) {
        this.listener = plistener
    }

    /**
     * 查询数据
     */
    fun searchpagesdata() {
        //创建数据查询对象(模拟数据查询)
        var datasearch = datasearch()
        //设置数据回调监听
        datasearch.setdatarequestlistener { errorcode, datas ->
            //切回主线程
            handler(looper.getmainlooper()).post {
                if (errorcode == 0) {
                    //累计当前位置
                    startindex += datas.size
                    //判断后面是否还有数据
                    var foottype = if (pagenum == datas.size) type_page_more 
                                   else type_page_last
                    //回调结果
                    listener.invoke(datas, foottype)
                } else {
                    //回调错误结果
                    listener.invoke(datas, type_page_error)
                }
            }
        }
        //查询数据
        datasearch.search(startindex, pagenum)
    }

    /**
     * 重置查询
     */
    fun reset() {
        startindex = 0;
    }
}
2.2、pageslayoutmanager 分页布局管理类

主要包含内容:

1、创建列表布局、网格布局(只创建一次即可)
2、记录数据位置(用于切换列表布局、网格布局时,保持位置不变)
package com.qxc.kotlinpages.pagemanage

import android.content.context
import androidx.recyclerview.widget.gridlayoutmanager
import androidx.recyclerview.widget.linearlayoutmanager

/**
 * 分页布局管理类:
 * 1、创建列表布局、网格布局
 * 2、记录数据位置(用于切换列表布局、网格布局时,保持位置不变)
 *
 * @author 齐行超
 * @date 19.11.30
 */
class pageslayoutmanager(
    pcontext: context
) {
    val style_list = 1 //列表样式(常量标识)
    val style_grid = 2 //网格样式(常量标识)

    var llmanager: linearlayoutmanager //列表布局管理器对象
    var glmanager: gridlayoutmanager //网格布局管理器对象

    var position: int = 0 //数据位置(当切换样式时,需记录数据的位置,用以保持数据位置不变)
    var context = pcontext //上下文对象

    init {
        llmanager = linearlayoutmanager(context)
        glmanager = gridlayoutmanager(context, 2)
    }

    /**
     * 获得布局管理器对象
     */
    fun getlayoutmanager(pagesstyle: int): linearlayoutmanager {
        //记录当前数据位置
        recorddataposition(pagesstyle)

        //根据样式返回布局管理器对象
        if (pagesstyle == style_list) {
            return llmanager
        }
        return glmanager
    }

    /**
     * 获得数据位置
     */
    fun getdataposition(): int {
        return position
    }

    /**
     * 记录数据位置
     */
    private fun recorddataposition(pagesstyle: int) {
        //pagesstyle表示目标样式,此处需要记录的是原样式时的数据位置
        if (pagesstyle == style_list) {
            position = glmanager?.findfirstvisibleitemposition()
        } else if (pagesstyle == style_grid) {
            position = llmanager?.findfirstvisibleitemposition()
        }
    }
}
2.3、pagesmanager 分页管理类

主要包含内容:

 1、创建、刷新适配器
 2、查询、绑定分页数据
 3、切换分页布局(列表布局、网格布局)
 4、当切换至网格布局时,设置footview独占一行(即使网格布局每行显示多个item,footview也独占一行)
package com.qxc.kotlinpages.pagemanage
import android.content.context
import androidx.recyclerview.widget.gridlayoutmanager
import androidx.recyclerview.widget.recyclerview
import com.qxc.kotlinpages.adapter.pagesadapter
import com.qxc.kotlinpages.pagedata.datainfo

/**
 * 分页管理类:
 * 1、创建适配器
 * 2、查询、绑定分页数据
 * 3、切换分页布局
 * 4、当切换至网格布局时,设置footview独占一行
 *
 * @author 齐行超
 * @date 19.11.30
 */
class pagesmanager(pcontext: context, precyclerview: recyclerview) {
    private var context = pcontext //上下文对象
    private var recyclerview = precyclerview //列表控件对象
    private var adapter:pagesadapter? = null //适配器对象
    private var pageslayoutmanager = pageslayoutmanager(context) //分页布局管理对象
    private var pagesdatamanager = pagesdatamanager() //分页数据管理对象
    private var datas = arraylist<datainfo>() //数据集合

    /**
     * 设置分页样式(列表、网格)
     *
     * @param isgrid 是否网格样式
     */
    fun setpagesstyle(isgrid: boolean): pagesmanager {
        //通过样式获得对应的布局类型
        var style = if (isgrid) pageslayoutmanager.style_grid 
                    else pageslayoutmanager.style_list
        var layoutmanager = pageslayoutmanager.getlayoutmanager(style)

        //获得当前数据位置(切换样式后,滑动到记录的数据位置)
        var position = pageslayoutmanager.getdataposition()

        //创建适配器对象
        adapter = pagesadapter(context, datas, pagesdatamanager.type_page_more)
        //通知适配器,itemview当前使用哪种分页布局样式(列表、网格)
        adapter?.setitemstyle(style)

        //列表控件设置适配器
        recyclerview.adapter = adapter
        //列表控件设置布局管理器
        recyclerview.layoutmanager = layoutmanager
        //列表控件滑动到指定位置
        recyclerview.scrolltoposition(position)

        //当layoutmanager是网格布局时,设置footview独占一行
        if(layoutmanager is gridlayoutmanager){
            setspansizelookup(layoutmanager)
        }

        //设置监听器
        setlistener()
        return this
    }

    /**
     * 设置监听器:
     *
     * 1、当滑动到列表底部时,查询下一页数据
     * 2、当点击了footview的"出错了,点击重试!!"时,重新查询数据
     */
    fun setlistener() {
        //1、当滑动到列表底部时,查询下一页数据
        adapter?.setonfootviewattachedtowindowlistener {
            //查询数据
            searchdata()
        }

        //2、当点击了footview的"出错了,点击重试!!"时,重新查询数据
        adapter?.setonfootviewclicklistener {
            if (it.equals(pagesdatamanager.type_page_error)) {

                //点击查询,更改footview状态
                adapter?.footmessage = pagesdatamanager.type_page_more
                adapter?.notifydatasetchanged()

                //"出错了,点击重试!!
                searchdata()
            }
        }
    }

    /**
     * 设置grid footview独占一行
     */
    fun setspansizelookup(layoutmanager: gridlayoutmanager) {
        layoutmanager.setspansizelookup(object : gridlayoutmanager.spansizelookup() {
            override fun getspansize(position: int): int {
                return if (adapter?.type_footview == adapter?.getitemviewtype(position)) 
                          layoutmanager.getspancount() 
                       else 1
            }
        })
    }

    /**
     * 获得查询结果,刷新列表
     */
    fun searchdata() {
        pagesdatamanager.setdatalistener { pdatas, footmessage ->
            if (pdatas != null) {
                datas.addall(pdatas)
                adapter?.footmessage = footmessage
                adapter?.notifydatasetchanged()
            }
        }
        pagesdatamanager.searchpagesdata()
    }
}

3、adapter 适配器模块

3.1、pagesadapter 分页适配器类

主要包括内容:

1、创建、绑定itemview(列表item、网格item)、footview
2、判断是否滑动到列表底部(更简单的方式实现列表滑动到底部的监听)
3、footview点击事件回调(如果是footview显示为“出错了,点击重试”,需要获取点击事件,重新查询数据)
4、滑动到列表底部事件回调(当列表滑动到底部时,则需要查询下一页数据了)
package com.qxc.kotlinpages.adapter

import android.content.context
import android.view.layoutinflater
import android.view.view
import android.view.viewgroup
import android.widget.textview
import androidx.recyclerview.widget.recyclerview
import com.qxc.kotlinpages.r
import com.qxc.kotlinpages.pagedata.datainfo

/**
 * 分页适配器类
 * 1、创建、绑定itemview(列表item、网格item)、footview
 * 2、判断是否滑动到列表底部
 * 3、footview点击事件回调
 * 4、滑动到列表底部事件回调
 *
 * @author 齐行超
 * @date 19.11.30
 */
class pagesadapter(
    pcontext: context,
    pdatainfos: arraylist<datainfo>,
    pfootmessage : string
) : recyclerview.adapter<recyclerview.viewholder>() {

    private var context = pcontext //上下文对象,通过构造传函数递过来
    private var datas = pdatainfos //数据集合,通过构造函数传递过来
    var footmessage = pfootmessage //footview文本信息,可通过构造函数传递过来,也可再次修改

    val type_footview: int = 1 //item类型:footview
    val type_itemview: int = 2 //item类型:itemview
    var typeitem = type_itemview

    val style_list_item = 1 //样式类型:列表
    val style_grid_item = 2 //样式类型:网格
    var styleitem = style_list_item

    /**
     * 重写创建viewholder的函数
     */
    override fun oncreateviewholder(parent: viewgroup, viewtype: int)
            : recyclerview.viewholder {
        //如果是itemview
        if (typeitem == type_itemview) {
            //判断样式类型(列表布局、网格布局)
            var layoutid =
                if (styleitem == style_list_item) r.layout.item_page_list 
                else r.layout.item_page_grid
            var view = layoutinflater.from(context).inflate(layoutid, parent, false)
            return itemviewholder(view)
        }
        //如果是footview
        else {
            var view = layoutinflater.from(context)
                            .inflate(r.layout.item_page_foot, parent, false)
            return footviewholder(view)
        }
    }

    /**
     * 重写获得项数量的函数
     */
    override fun getitemcount(): int {
        //因列表中增加了footview(显示分页状态信息),所以item总数量 = 数据数量 + 1
        return datas.size + 1
    }

    /**
     * 重写绑定viewholder的函数
     */
    override fun onbindviewholder(holder: recyclerview.viewholder, position: int) {
        if (holder is itemviewholder) {
            if (datas.size <= position) {
                return
            }
            var data = datas.get(position)
            holder.tv_title.text = data.title
            holder.tv_desc.text = data.desc
            holder.tv_price.text = data.price
            holder.tv_link.text = data.link

        } else if (holder is footviewholder) {
            holder.tv_msg.text = footmessage

            //当点击footview时,将该事件回调出去
            holder.tv_msg.setonclicklistener {
                footviewclicklistener.invoke(footmessage)
            }
        }
    }

    /**
     * 重新获得项类型的函数(项类型包括:itemview、footview)
     */
    override fun getitemviewtype(position: int): int {
        //设置在数据最底部显示footview
        typeitem = if (position == datas.size) type_footview else type_itemview
        return typeitem
    }


    /**
     * 当footview第二次出现在列表时,回调该事件
     * (此处用于模拟用户上滑手势,当滑到底部时,重新请求数据)
     */
    var footviewposition = 0
    override fun onviewattachedtowindow(holder: recyclerview.viewholder) {
        super.onviewattachedtowindow(holder)
        if (footviewposition == holder.adapterposition) {
            return
        }
        if (holder is footviewholder) {
            footviewposition = holder.adapterposition
            //回调查询事件
            footviewattachedtowindowlistener.invoke()
        }
    }

    /**
     * itemviewholder
     */
    class itemviewholder(itemview: view) : recyclerview.viewholder(itemview) {
        var tv_title = itemview.findviewbyid<textview>(r.id.tv_title)
        var tv_desc = itemview.findviewbyid<textview>(r.id.tv_desc)
        var tv_price = itemview.findviewbyid<textview>(r.id.tv_price)
        var tv_link = itemview.findviewbyid<textview>(r.id.tv_link)
    }

    /**
     * footviewholder
     */
    class footviewholder(itemview: view) : recyclerview.viewholder(itemview) {
        var tv_msg = itemview.findviewbyid<textview>(r.id.tv_msg)
    }

    /**
     * 设置item样式(列表、网格)
     */
    fun setitemstyle(pstyle: int) {
        styleitem = pstyle
    }

    //定义footview附加到window上时的回调
    lateinit var footviewattachedtowindowlistener: () -> unit
    fun setonfootviewattachedtowindowlistener(plistener: () -> unit) {
        this.footviewattachedtowindowlistener = plistener
    }

    //定义footview点击时的回调
    lateinit var footviewclicklistener:(string)->unit
    fun setonfootviewclicklistener(plistner:(string)->unit){
        this.footviewclicklistener = plistner
    }
}

4、utils 工具模块

4.1、apputils 项目工具类

此案例中主要用于判断网络连接情况:

package com.qxc.kotlinpages.utils

import android.content.context
import android.net.connectivitymanager
import android.net.networkcapabilities
import android.os.build

/**
 * 工具类
 *
 * @author 齐行超
 * @date 19.11.30
 */
class apputils {
    //使用共生对象,表示静态static
    companion object{
        /**
         * 线程安全的单例(懒汉式单例)
         */
        val instance : apputils by lazy(mode = lazythreadsafetymode.synchronized) {
            apputils()
        }

        private lateinit var context:context

        /**
         * 注册
         *
         * @param pcontext 上下文
         */
        fun register(pcontext: context){
            context = pcontext
        }
    }

    /**
     * 判断是否连接了网络
     */
    fun isconnectnetwork():boolean{
        var result = false
        val cm = context.getsystemservice(context.connectivity_service) 
                 as connectivitymanager?
        if (build.version.sdk_int >= build.version_codes.p) {
            cm?.run {
                this.getnetworkcapabilities(cm.activenetwork)?.run {
                    result = when {
                        this.hastransport(networkcapabilities.transport_wifi) -> true
                        this.hastransport(networkcapabilities.transport_cellular) -> true
                        this.hastransport(networkcapabilities.transport_ethernet) -> true
                        else -> false
                    }
                }
            }
        } else {
            cm?.run {
                cm.activenetworkinfo?.run {
                    if (type == connectivitymanager.type_wifi) {
                        result = true
                    } else if (type == connectivitymanager.type_mobile) {
                        result = true
                    }
                }
            }
        }
        return result
    }
}

5、ui模块

5.1、mainactivity 主页面,用于显示分页列表、切换分页样式(列表样式、网格样式)
package com.qxc.kotlinpages

import androidx.appcompat.app.appcompatactivity
import android.os.bundle
import com.qxc.kotlinpages.pagemanage.pagesmanager
import com.qxc.kotlinpages.utils.apputils
import kotlinx.android.synthetic.main.activity_main.*

class mainactivity : appcompatactivity() {
    var isgrid = false
    var pagesmanager: pagesmanager? = null

    override fun oncreate(savedinstancestate: bundle?) {
        super.oncreate(savedinstancestate)
        setcontentview(r.layout.activity_main)
        apputils.register(this)

        initevent()
        initdata()
    }

    fun initevent() {
        //切换列表样式按钮的点击事件
        iv_style.setonclicklistener {
            //切换图标(列表与网格)
            var id: int =
                if (isgrid) r.mipmap.product_search_list_style_grid 
                else r.mipmap.product_search_list_style_list
            iv_style.setimageresource(id)

            //记录当前图标类型
            isgrid = !isgrid

            //更改样式(列表与网格)
            pagesmanager!!.setpagesstyle(isgrid)
        }
    }

    fun initdata() {
        //初始化pagesmanager,默认查询列表
        pagesmanager = pagesmanager(this, rv_data)
        pagesmanager!!.setpagesstyle(isgrid).searchdata()
    }
}
注意:页面中引用了 kotlinx.android.synthetic.main.activity_main.*
      》》这表示无需再写findviewbyid()了,直接使用xml中控件id即可

mainactivity的布局页面,使用了约束布局,层级嵌套少,且更简单一些:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.constraintlayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".mainactivity">

    <view
        android:id="@+id/v_top"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:background="#fd4d4d"
        app:layout_constraintleft_toleftof="parent"
        app:layout_constraintright_torightof="parent"
        app:layout_constrainttop_totopof="parent" />

    <textview
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="分页demo"
        android:textcolor="#ffffff"
        android:textsize="18sp"
        app:layout_constraintbottom_tobottomof="@id/v_top"
        app:layout_constraintleft_toleftof="@id/v_top"
        app:layout_constraintright_torightof="@id/v_top"
        app:layout_constrainttop_totopof="@id/v_top" />

    <imageview
        android:id="@+id/iv_style"
        android:layout_width="40dp"
        android:layout_height="40dp"
        android:layout_marginright="5dp"
        android:scaletype="center"
        android:src="@mipmap/product_search_list_style_grid"
        app:layout_constraintbottom_tobottomof="@id/v_top"
        app:layout_constraintright_torightof="@id/v_top"
        app:layout_constrainttop_totopof="@id/v_top" />

    <androidx.recyclerview.widget.recyclerview
        android:id="@+id/rv_data"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        app:layout_constraintbottom_tobottomof="parent"
        app:layout_constraintleft_toleftof="parent"
        app:layout_constraintright_torightof="parent"
        app:layout_constrainttop_tobottomof="@id/v_top" />

</androidx.constraintlayout.widget.constraintlayout>
5.2、item布局(列表样式),也是使用了约束布局:

Kotlin实战案例:带你实现RecyclerView分页查询功能(仿照主流电商APP,可切换列表和网格效果)

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.constraintlayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="150dp"
    android:layout_marginleft="10dp"
    android:layout_margintop="10dp"
    android:layout_marginright="10dp"
    android:background="#eeeeee">

    <imageview
        android:id="@+id/iv_image"
        android:layout_width="80dp"
        android:layout_height="110dp"
        android:layout_marginleft="10dp"
        android:scaletype="fitxy"
        android:src="@mipmap/kotlin"
        app:layout_constraintbottom_tobottomof="parent"
        app:layout_constraintleft_toleftof="parent"
        app:layout_constrainttop_totopof="parent" />

    <textview
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginleft="10dp"
        android:text="android kotlin"
        android:textcolor="#333333"
        android:textsize="18sp"
        app:layout_constraintleft_torightof="@id/iv_image"
        app:layout_constrainttop_totopof="@id/iv_image" />

    <textview
        android:id="@+id/tv_desc"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginleft="10dp"
        android:layout_marginright="10dp"
        android:lines="2"
        android:text="kotlin 是一个用于现代多平台应用的静态编程语言,由 jetbrains 开发..."
        android:textcolor="#888888"
        android:textsize="12sp"
        app:layout_constraintleft_torightof="@id/iv_image"
        app:layout_constraintright_torightof="parent"
        app:layout_constrainttop_tobottomof="@id/tv_title" />

    <textview
        android:id="@+id/tv_price_symbol"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginleft="10dp"
        android:layout_margintop="20dp"
        android:text="¥"
        android:textcolor="#fd4d4d"
        android:textsize="10dp"
        app:layout_constraintleft_torightof="@id/iv_image"
        app:layout_constrainttop_tobottomof="@id/tv_desc" />

    <textview
        android:id="@+id/tv_price"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="128.00"
        android:textcolor="#fd4d4d"
        android:textsize="22sp"
        app:layout_constraintbaseline_tobaselineof="@id/tv_price_symbol"
        app:layout_constraintleft_torightof="@id/tv_price_symbol" />

    <textview
        android:id="@+id/tv_link"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginleft="10dp"
        android:text="跳转至kotlin柜台 -> jetbrains"
        android:textcolor="#aaaaaa"
        android:textsize="10sp"
        app:layout_constraintbottom_tobottomof="@id/iv_image"
        app:layout_constraintleft_torightof="@id/iv_image" />

</androidx.constraintlayout.widget.constraintlayout>
5.3、item布局(网格样式),仍然使用了约束布局:

Kotlin实战案例:带你实现RecyclerView分页查询功能(仿照主流电商APP,可切换列表和网格效果)

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.constraintlayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:layout_marginleft="10dp"
    android:layout_margintop="10dp"
    android:layout_marginright="10dp"
    android:paddingbottom="10dp"
    android:background="#eeeeee">

    <imageview
        android:id="@+id/iv_image"
        android:layout_width="80dp"
        android:layout_height="110dp"
        android:layout_margintop="10dp"
        android:scaletype="fitxy"
        android:src="@mipmap/kotlin"
        app:layout_constraintleft_toleftof="parent"
        app:layout_constraintright_torightof="parent"
        app:layout_constrainttop_totopof="parent" />

    <textview
        android:id="@+id/tv_title"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginleft="10dp"
        android:layout_margintop="20dp"
        android:text="android kotlin"
        android:textcolor="#333333"
        android:textsize="18sp"
        app:layout_constraintleft_toleftof="parent"
        app:layout_constrainttop_tobottomof="@id/iv_image" />

    <textview
        android:id="@+id/tv_desc"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginright="10dp"
        android:lines="2"
        android:text="kotlin 是一个用于现代多平台应用的静态编程语言,由 jetbrains 开发..."
        android:textcolor="#888888"
        android:textsize="12sp"
        app:layout_constraintleft_toleftof="@id/tv_title"
        app:layout_constraintright_torightof="parent"
        app:layout_constrainttop_tobottomof="@id/tv_title" />

    <textview
        android:id="@+id/tv_price_symbol"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margintop="20dp"
        android:text="¥"
        android:textcolor="#fd4d4d"
        android:textsize="10dp"
        app:layout_constraintleft_toleftof="@id/tv_title"
        app:layout_constrainttop_tobottomof="@id/tv_desc" />

    <textview
        android:id="@+id/tv_price"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="128.00"
        android:textcolor="#fd4d4d"
        android:textsize="22sp"
        app:layout_constraintbaseline_tobaselineof="@id/tv_price_symbol"
        app:layout_constraintleft_torightof="@id/tv_price_symbol" />

    <textview
        android:id="@+id/tv_link"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="跳转至kotlin柜台 -> jetbrains"
        android:textcolor="#aaaaaa"
        android:textsize="10sp"
        app:layout_constrainttop_tobottomof="@id/tv_price"
        app:layout_constraintleft_toleftof="@id/tv_title" />

</androidx.constraintlayout.widget.constraintlayout>
5.4、footview布局

比较简单,仅有一个文本控件:

Kotlin实战案例:带你实现RecyclerView分页查询功能(仿照主流电商APP,可切换列表和网格效果)

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.constraintlayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="50dp"
    android:layout_margin="10dp"
    android:background="#eeeeee">

    <textview
        android:id="@+id/tv_msg"
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:gravity="center"
        android:text="加载中..."
        android:textcolor="#777777"
        android:textsize="12sp"
        app:layout_constraintbottom_tobottomof="parent"
        app:layout_constraintleft_toleftof="parent"
        app:layout_constraintright_torightof="parent"
        app:layout_constrainttop_totopof="parent" />

</androidx.constraintlayout.widget.constraintlayout>

总结

分页实现难点汇总:

1、切换recyclerview展示样式(列表样式、网格样式),保持数据位置不变
2、网格样式时,footview独占一行
3、直接在adapter中判断是否滑动到了底部,比常规做法(监听滑动坐标)更简单一些
4、分页状态管控(数据加载中、没有更多数据了、出错了点击重试)

kotlin主要技术点汇总:

1、多线程实现(lambda表达式的应用)
2、异步回调(lambda表达式的应用、高阶函数)
3、共生对象
4、线程安全单例
5、其他略(都比较基础了,大家熟悉下即可)

此篇文章主要是为了讲解常规分页的实现,有些内容并未进行完全解耦,如果想在项目中使用,建议还是抽象一下,扩展性会更好一些。

如果有疑问,也欢迎留言咨询o(∩_∩)o~

demo下载地址:
https://pan.baidu.com/s/1gh0zcd0qxdm4mrnmqjgs8q