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

Kotlin入门(23)适配器的进阶表达

程序员文章站 2022-05-18 22:32:40
前面在介绍列表视图和网格视图时,它们的适配器代码都存在视图持有者ViewHolder,因为Android对列表类视图提供了回收机制,如果某些列表项在屏幕上看不到了,则系统会自动回收相应的视图对象。随着用户的下拉或者上拉手势,已经被回收的列表项要重新加载到界面上,倘若每次加载都得从头创建视图对象,势必 ......

前面在介绍列表视图和网格视图时,它们的适配器代码都存在视图持有者viewholder,因为android对列表类视图提供了回收机制,如果某些列表项在屏幕上看不到了,则系统会自动回收相应的视图对象。随着用户的下拉或者上拉手势,已经被回收的列表项要重新加载到界面上,倘若每次加载都得从头创建视图对象,势必增加了系统的资源开销。所以viewholder便应运而生,它在列表项首次初始化时,就将其视图对象保存起来,后面再次加载该视图时,即可直接从持有者处获得先前的视图对象,从而减少了系统开销,提高了系统的运行效率。
视图持有者的设计理念固然美好,却苦了android开发者,每次由baseadapter派生新的适配器类,都必须手工处理视图持有者的相关逻辑,实在是个沉重的负担。有鉴于此,循环视图的适配器把视图持有者的重用逻辑剥离出来,由系统自行判断并处理持有者的重用操作。开发者继承recyclerview.adapter之后,只要完成业务上的代码逻辑即可,无需进行baseadapter视图持有者的手工重用。
现在由kotlin实现循环视图的适配器类,综合前面两小节提到的优化技术,加上视图持有者的自动重用,适配器代码又得到了进一步的精简。由于循环视图适配器并不提供列表项的点击事件,因此开发者要自己编写包括点击、长按在内的事件处理代码。为方便理解循环适配器的kotlin编码,下面以微信的公众号消息列表为例,给出对应的消息列表kotlin代码:

//viewholder在构造时初始化布局中的控件对象
class recyclerlinearadapter(private val context: context, private val infos: mutablelist<recyclerinfo>) : recyclerview.adapter<viewholder>(), onitemclicklistener, onitemlongclicklistener {
    val inflater: layoutinflater = layoutinflater.from(context)

    //获得列表项的数目
    override fun getitemcount(): int = infos.size

    //创建整个布局的视图持有者
    override fun oncreateviewholder(parent: viewgroup, viewtype: int): viewholder {
        val view: view = inflater.inflate(r.layout.item_recycler_linear, parent, false)
        return itemholder(view)
    }

    //绑定每项的视图持有者
    override fun onbindviewholder(holder: viewholder, position: int) {
        val vh: itemholder = holder as itemholder
        vh.iv_pic.setimageresource(infos[position].pic_id)
        vh.tv_title.text = infos[position].title
        vh.tv_desc.text = infos[position].desc
        // 列表项的点击事件需要自己实现
        vh.ll_item.setonclicklistener { v ->
            itemclicklistener?.onitemclick(v, position)
        }
        vh.ll_item.setonlongclicklistener { v ->
            itemlongclicklistener?.onitemlongclick(v, position)
            true
        }
    }

    //itemholder中的属性在构造时初始化
    inner class itemholder(view: view) : recyclerview.viewholder(view) {
        var ll_item = view.findviewbyid(r.id.ll_item) as linearlayout
        var iv_pic = view.findviewbyid(r.id.iv_pic) as imageview
        var tv_title = view.findviewbyid(r.id.tv_title) as textview
        var tv_desc = view.findviewbyid(r.id.tv_desc) as textview
    }

    private var itemclicklistener: onitemclicklistener? = null
    fun setonitemclicklistener(listener: onitemclicklistener) {
        this.itemclicklistener = listener
    }

    private var itemlongclicklistener: onitemlongclicklistener? = null
    fun setonitemlongclicklistener(listener: onitemlongclicklistener) {
        this.itemlongclicklistener = listener
    }

    override fun onitemclick(view: view, position: int) {
        val desc = "您点击了第${position+1}项,标题是${infos[position].title}"
        context.toast(desc)
    }

    override fun onitemlongclick(view: view, position: int) {
        val desc = "您长按了第${position+1}项,标题是${infos[position].title}"
        context.toast(desc)
    }
}

以上的适配器代码初步实现了公众号消息列表的展示页面,具体的列表效果如下图所示。

Kotlin入门(23)适配器的进阶表达

可是这个循环适配器recyclerlinearadapter仍然体量庞大,细细观察发现其实它有着数个与具体业务无关的属性与方法,譬如上下文对象context、布局载入对象inflater、点击监听器itemclicklistener、长按监听器itemlongclicklistener等等,故而完全可以把这些通用部分提取到一个基类,然后具体业务再从该基类派生出特定的业务适配器类。根据这种设计思路,提取出了循环视图基础适配器,它的kotlin代码如下所示:

//循环视图基础适配器
abstract class recyclerbaseadapter<vh : recyclerview.viewholder>(val context: context) : recyclerview.adapter<recyclerview.viewholder>(), onitemclicklistener, onitemlongclicklistener {
    val inflater: layoutinflater = layoutinflater.from(context)

    //获得列表项的个数,需要子类重写
    override abstract fun getitemcount(): int

    //根据布局文件创建视图持有者,需要子类重写
    override abstract fun oncreateviewholder(parent: viewgroup, viewtype: int): recyclerview.viewholder

    //绑定视图持有者中的各个控件对象,需要子类重写
    override abstract fun onbindviewholder(holder: recyclerview.viewholder, position: int)

    override fun getitemviewtype(position: int): int = 0

    override fun getitemid(position: int): long = position.tolong()

    var itemclicklistener: onitemclicklistener? = null
    fun setonitemclicklistener(listener: onitemclicklistener) {
        this.itemclicklistener = listener
    }

    var itemlongclicklistener: onitemlongclicklistener? = null
    fun setonitemlongclicklistener(listener: onitemlongclicklistener) {
        this.itemlongclicklistener = listener
    }

    override fun onitemclick(view: view, position: int) {}

    override fun onitemlongclick(view: view, position: int) {}
}

一旦有了这个基础适配器,实际业务的适配器即可由此派生而来,真正需要开发者编写的代码一下精简了不少。下面便是个循环视图的网格适配器,它实现了类似淘宝主页的网格频道栏目,具体的kotlin代码如下所示:

//把公共属性和公共方法剥离到基类recyclerbaseadapter,
//此处仅需实现getitemcount、oncreateviewholder、onbindviewholder三个方法,以及视图持有者的类定义
class recyclergridadapter(context: context, private val infos: mutablelist<recyclerinfo>) : recyclerbaseadapter<recyclerview.viewholder>(context) {

    override fun getitemcount(): int = infos.size

    override fun oncreateviewholder(parent: viewgroup, viewtype: int): recyclerview.viewholder {
        val view: view = inflater.inflate(r.layout.item_recycler_grid, parent, false)
        return itemholder(view)
    }

    override fun onbindviewholder(holder: recyclerview.viewholder, position: int) {
        val vh = holder as itemholder
        vh.iv_pic.setimageresource(infos[position].pic_id)
        vh.tv_title.text = infos[position].title
    }

    inner class itemholder(view: view) : recyclerview.viewholder(view) {
        var ll_item = view.findviewbyid(r.id.ll_item) as linearlayout
        var iv_pic = view.findviewbyid(r.id.iv_pic) as imageview
        var tv_title = view.findviewbyid(r.id.tv_title) as textview
    }
}

改进后的循环网格适配器,运行之后的界面效果如下图所示,无缝实现了原来需要数十行java代码才能实现的功能。

Kotlin入门(23)适配器的进阶表达

然而基类不过是雕虫小技,java也照样能够运用,所以这根本不入kotlin的法眼,要想超越java,还得拥有独门秘笈才行。注意到适配器代码仍然通过findviewbyid方法获得控件对象,可是号称在anko库的支持之下,kotlin早就无需该方法就能直接访问控件对象了呀,为啥这里依旧靠老牛拉破车呢?其中的缘由是anko库仅仅实现了activity活动页面的控件自动获取,并未实现适配器内部的自动获取。不过kotlin早就料到了这一手,为此专门提供了一个插件名叫layoutcontainer,只要开发者让自定义的viewholder继承该接口,即可在视图持有者内部无需获取就能使用控件对象了。这下不管是在activity代码,还是在适配器代码中,均可将控件名称拿来直接调用了。这么神奇的魔法,快来看看kotlin的适配器代码是如何书写的:

//利用kotlin的插件layoutcontainer,在适配器中直接使用控件对象,而无需对其进行显式声明
class recyclerstaggeredadapter(context: context, private val infos: mutablelist<recyclerinfo>) : recyclerbaseadapter<recyclerview.viewholder>(context) {

    override fun getitemcount(): int = infos.size

    override fun oncreateviewholder(parent: viewgroup, viewtype: int): recyclerview.viewholder {
        val view: view = inflater.inflate(r.layout.item_recycler_staggered, parent, false)
        return itemholder(view)
    }

    override fun onbindviewholder(holder: recyclerview.viewholder, position: int) {
        (holder as itemholder).bind(infos[position])
    }

    //注意这里要去掉inner,否则运行报错“java.lang.nosuchmethoderror: no virtual method _$_findcachedviewbyid”
    class itemholder(override val containerview: view?) : recyclerview.viewholder(containerview), layoutcontainer {
        fun bind(item: recyclerinfo) {
            iv_pic.setimageresource(item.pic_id)
            tv_title.text = item.title
        }
    }
}

当然,为了能够正常使用该功能,需要在适配器代码头部加上以下两行代码,其中第一行代码表示引用了kotlin的扩展插件layoutcontainer,第二行代码与activity的一样表示导入了指定布局文件里面所有控件对象:

import kotlinx.android.extensions.layoutcontainer
import kotlinx.android.synthetic.main.item_recycler_staggered.*

 

另外,因为layoutcontainer是kotlin针对性提供给android的扩展插件,所以需要修改模块的build.gradle,在文件末尾添加下面几行配置,表示允许引用安卓插件库:

androidextensions {
    experimental = true
}

 

即使修改后的适配器代码用了新插件,外部仍旧同原来一样给循环视图设置适配器,调用代码并无任何变化:

    //第一种方式:使用采取了layoutcontainer的插件适配器
    val adapter = recyclerstaggeredadapter(this, recyclerinfo.defaultstag)
    rv_staggered.adapter = adapter

采用了新的适配器插件,似乎已经大功告成,可是依然要书写单独的适配器代码,仔细研究发现这个recyclerstaggeredadapter还有三个要素是随着具体业务而变化的,包括:

1、列表项的布局文件资源编码,如r.layout.item_recycler_staggered;
2、列表项信息的数据结构名称,如recyclerinfo;
3、对各种控件对象的设置操作,如itemholder类的bind方法;
除了以上三个要素,recyclerstaggeredadapter内部的其余代码都是允许复用的,因此,接下来的工作就是想办法把这三个要素抽象为公共类的某种变量。对于第一个的布局编码,可以考虑将其作为一个整型的输入参数;对于第二个的数据结构,可以考虑定义一个模板类,在外部调用时再指定具体的数据类;对于第三个的bind方法,若是java编码早已束手无策,现用kotlin编码正好将该方法作为一个函数参数传入。依照三个要素的三种处理对策,进而提炼出来了循环适配器的通用类recyclercommonadapter,详细的kotlin代码示例如下:

//循环视图通用适配器
//将具体业务中会变化的三类要素抽取出来,作为外部传进来的变量。这三类要素包括:
//布局文件对应的资源编号、列表项的数据结构、各个控件对象的初始化操作
class recyclercommonadapter<t>(context: context, private val layoutid: int, private val items: list<t>, val init: (view, t) -> unit): recyclerbaseadapter<recyclercommonadapter.itemholder<t>>(context) {

    override fun getitemcount(): int = items.size

    override fun oncreateviewholder(parent: viewgroup, viewtype: int): recyclerview.viewholder {
        val view: view = inflater.inflate(layoutid, parent, false)
        return itemholder<t>(view, init)
    }

    override fun onbindviewholder(holder: recyclerview.viewholder, position: int) {
        val vh: itemholder<t> = holder as itemholder<t>
        vh.bind(items.get(position))
    }

    //注意init是个函数形式的输入参数
    class itemholder<in t>(val view: view, val init: (view, t) -> unit) : recyclerview.viewholder(view) {
        fun bind(item: t) {
            init(view, item)
        }
    }
}

有了这个通用适配器,外部使用适配器只需像函数调用那样传入这三种变量就好了,具体调用的kotlin代码如下所示:

    //第二种方式:使用把三类可变要素抽象出来的通用适配器
    val adapter = recyclercommonadapter(this, r.layout.item_recycler_staggered, recyclerinfo.defaultstag,
            {view, item ->
                val iv_pic = view.findviewbyid(r.id.iv_pic) as imageview
                val tv_title = view.findviewbyid(r.id.tv_title) as textview
                iv_pic.setimageresource(item.pic_id)
                tv_title.text = item.title
            })
    rv_staggered.adapter = adapter

 

最终出炉的适配器仅有十行代码不到,其中的关键技术——函数参数真是不鸣则已、一鸣惊人。至此本节的适配器实现过程终于落下帷幕,一路上可谓是过五关斩六将,硬生生把数十行的java代码压缩到不到十行的kotlin代码,经过不断迭代优化方取得如此彪炳战绩。尤其是最后的两种实现方式,分别运用了kotlin的多项综合技术,才能集kotlin精妙语法之大成。