• 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应独占一行

    设计

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

    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类有两个重点知识:

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

    a、参考常规分页网络请求API,数据查询方法应包含参数:起始位置、每页数量
    b、安卓中的网络查询,需要在子线程中操作,当前案例直接使用Thread{}.start()实现子线程
    
    线程实现的写法与Java中不太一样,为什么这么写呢?咱们具体展开说明一下:
    -----------------------------------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
    

    1.2.2、数据回调监听

    此案例通过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也独占一行)
    

    主要技术点,包括:

    1、设置grid footview独占一行
    2、RecyclerView控件的使用(数据绑定,刷新,样式切换等)
    
    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、滑动到列表底部事件回调(当列表滑动到底部时,则需要查询下一页数据了)
    

    主要技术点,包括:

    1、多item项的应用
    2、滑动到列表底部的判断(比“监听RecyclerView的Scroll坐标”这种常规做法要简化很多,且精准)
    
    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 项目工具类

    此案例中主要用于判断网络连接情况。
    该类的主要技术点:Kotlin的共生对象、线程安全单例,详见源码:

    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布局(列表样式),也是使用了约束布局:

    <?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布局(网格样式),仍然使用了约束布局:

    <?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布局

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

    <?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、其他略(都比较基础了,大家熟悉下即可)

    此篇文章主要是为了讲解常规分页的实现,所以只是做了一些基础的拆分解耦,如果想在项目中使用,建议还是抽象一下,扩展性会更好一些(如:footview接口化扩展、数据查询接口化扩展等)。

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

    Demo下载地址:
    https://pan.baidu.com/s/1gH0Zcd0QXdm4mRNMqJgS8Q

  • 相关阅读:
    Information retrieval (IR class2)
    HTML随笔
    Evaluating Automatically Generated timelines from the Web (paper1)
    Kali 2020.1版本安装
    SystemTap
    Linux之IDIDID
    调试&内核探针
    Return-to-dl-resolve
    转载!
    一张图系列之函数重定位
  • 原文地址:https://www.cnblogs.com/qixingchao/p/11978229.html
Copyright © 2020-2023  润新知