随着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) -> Unit /** * 设置数据请求监听器 * * @param plistener 数据请求监听器的回调类对象 */ fun setDataRequestListener(plistener: (errorCode: Int, dataInfos: ArrayList) -> 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() //判断网络状态 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) -> 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, footType: String) -> Unit) /** * 设置回调 */ fun setDataListener(pListener: (dataInfos: ArrayList, 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() //数据集合 /** * 设置分页样式(列表、网格) * * @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, pFootMessage : String ) : RecyclerView.Adapter() { private var context = pContext //上下文对象,通过构造传函数递过来 private var datas = pDataInfos //数据集合,通过构造函数传递过来