构建Recyclerview DSL

作者:RetroX
原文链接:https://github.com/life2015/RecyclerViewDSL/blob/master/README_ZH.md

接文章 DSL in action

上一篇文章说了如何把DSL用在项目的布局中,而这篇文章来讲讲怎么把DSL用在Recyclerview中。此框架已经在我的项目中大规模使用,并且极大地提高了Recyclerview列表构建效率和复用能力。

特色

  • 轻量级(只有一个Kotlin文件)
  • 可拓展(你可以完全自定义自己的Item)
  • 易用(它只是对Rec的OnCreateVH OnBindVH做了代理,不需要额外的学习成本)
  • 写着爽(Anko风格写法,DSL配置列表灵活易用)

看看效果?

这是一个大概的效果,Recyclerview DSL中,我们可以用DSL的风格去配置Item被如何加入到Rec,各个Item的风格是什么样子,具有很大的灵活性和拓展性。

核心类概览

  • Item: Recyclerview DSL中,用来保存View对应数据的类,比如说TextView的字符串,Imageview的url等等,基本上可以认为是担任着ViewModel的角色
  • ItemController: 一般内嵌在Item类的Companion Object中,用于代理Item相关的OnCreateVH,OnBindVH逻辑,基本上一个Item的View逻辑和业务逻辑在这里表现。
  • ItemAdapter:Recyclerview DSL所依赖的Adapter,在初始化的时候会用到,后面它很少出面了
  • ItemManager: RecyclerView DSL的Adapter的一个核心成员变量,统管着Adapter的Item和相应的ItemController,比如说他们的刷新,添加,删除。DSL的语法特性拓展,基本上在这里表现。

那怎么用?

  • 定义列表要用的Item(可以全局复用 所以要好好设计)
  • 写一个MutableList的拓展
  • 开始使用!

举个栗子?

比如说我要写定义一类Item,这类Item就是一个FrameLayout里面包了个TextView。

然后怎么写呢?

  1. 先定义一个Item,我们就叫它SingleTextItem.kt
    这个Item里面需要包含一个字符串,将来在OnBindVH的代理中传入到View
  2. 然后我们需要些这类Item对于的逻辑,也就是ItemController,在伴生对象中进行实现
  3. 写个拓展函数,来让它支持DSL
  4. 来试试把,用一下~

复杂情景讨论

情景1: 同一个Item下,对于ViewStyle的不同处理

方案:Item中除了必要的数据类,再传入一个 YourView.() -> Unit类型的可空?闭包。

原理蛮简单,就弄代码了,注释很全… 还是中英双语的呢

情景2 : 可刷新列表

比如说,分页加载,列表变化,和其他所有可变的Recyclerview列表

方案:这种情况下,我们把ItemManager拿出来单独操作即可,善用autorefresh方法和DiffUtil

想要更加好的刷新体验,就要先给给RecyclerviewDSL加入DiffUtil的能力 :

实现Item接口的时候, 重写后面那俩默认方法即可。
比如说我们要做一个列表,列表里面是一堆文字的item,在最末尾有一个Button,点击Button就会让文字Item添加10个。然后在autoRefresh的闭包中,我们只需要用DSL来表达这个需求即可。框架会帮我们做这一切。

AutoRefresh背后的原理就是,在调用闭包前,对Adapter的Item做一个SnapShot,然后对比AutoRefresh闭包使用之后的ItemList情况,最后使用DiffUtil来处理。

如果你是要对列表进行全量刷新,可以直接使用refreshll方法,此方法会清除列表然后再添加新的Item,当然这个过程是有DiffUtil参与的。

原理/动机分析

常规开发

如果按照普通的开发流程,构建列表的时候,一般就是 Adapter + List。 Adapter里面包含着ViewHolder的创建和绑定逻辑,这样子在大规模开发迭代中会遇到的一个问题是:Adapter的逻辑越堆积越重,比如说在OnBindViewHolder方法中包含着重度的业务逻辑,getItemViewType,onCreateViewHolder中包含着大量的样板代码。

  • 定义ViewType常量
  • getItemViewType中各种判断
  • OnCreateViewHolder中做创建
  • OnBindViewHolder做数据绑定

这些代码都会堆积在Adapter中,时间一长,Type一多,Adapter写起来就会很蛋疼。另外,ViewType/ViewHolder/BindViewHolder逻辑都很难去复用,因为他们是写死在ViewHolder里面的。

简单优化一下?

我们开始思考,这些东西是不是可以解耦开呢?

于是你觉得,OnBindViewHolder的逻辑可以写在ViewHolder里面,然后

在这种架构下,可以把ViewHolder独立开,解耦一部分Adapter中的逻辑。嗯… 还可以(没啥技术含量)

问题/不足

  • ViewHolder复用问题:
    我们只解耦了OnBindViewHolder的逻辑,但OnCreateViewHolder还是要再写
  • 复用灵活性问题:
    比如说我在复用的时候,Adapter1里面对CardView要设置1dp的阴影,Adapter2里面需要3dp。
    Adapter1里面对这类ViewHolder里面的TextView要设置:字体,颜色,字号。Adapter2里面需要另外的配置。
    又比如说,Adapter1里面对于不同地方的同类ViewHolder里面的TextView要设置:字体,颜色,字号等等….
  • ViewType问题:
    我们真的需要手动指定ViewType吗,因为经过我的一番思考,ViewType和ViewHolder::class.java在合理的封装下,可以是1对1的关系。

再次思考 – 到底要怎么解耦?

于是我开始思考在Recyclerview的架构中,确定一类视图到底需要什么?哪些东西可以用一个最小的集合来定义一类视图?

我们来梳理一下:

所以说,只要我们把OnCreateVH,OnBindVH的逻辑代理出去,就可以把一类Item的视图部分进行完整的解耦。给太子端代码!

现在我们解耦出了视图,还剩下视图的数据填充。一般来讲,Model数据类型和ViewHolder类型一一对应,因此我们可以认为一种ItemController对应着一个类型的Item(一般就是嵌入的一个data Class)

于是我们把数据类嵌入进去

比如说我们有一个高度定制的TextView

在这里,我们就已经把IndicatorTextView这个Recyclerview Item的视图层和数据填充都解耦了出来。只需要塞进去IndicatorTextItem对象,就可以做到相应的效果。并且这个Item可以在多个Recyclerview Adapter中复用。

Adapter如何协调?

与这套解耦相配合的是一套Adapter的封装,来对接相关的接口完成对应逻辑的解耦已经ViewType的分配

对于Adapter,我们需要完成的逻辑就是 ItemController <----> ViewType的转换。

一个理论前提是:在高度封装的情况下,ViewType并没有具体的语义,它的作用在于区分不同的ItemController。而对于具体的语义,则转到Item那边来表示,比如说上面的class IndicatorTextItem(val text: String) : Item

落实到方法上:我们可以实现一套ItemController <----> ViewType的注册机制,那么这套机制的具体需求是什么?应该怎样设计?先列下需求:

  • 一对一的关系 支持相互索引
  • 照顾ViewHolder的全局复用
  • ViewType自动生成
  • 添加Item时自动注册

一对一的关系 支持相互索引:我们可以维护两个Map

因为要保证Key,Value的相互之前快速索引,因此需要同时管理这两个Map。

添加Item时自动注册 + ViewType自动生成 :Item接口要求必须有一个controller成员变量,因此在添加到Item List的同时,进行监听。不如来看看代码

在Adapter 的数据源修改时,调用相关的ensureControllers方法来完成相关的注册。同时Adapter中,相关的逻辑也可以被这里的ItemController代理,代码差不多是这样子的:

在这种情况下,Adapter的两个核心方法就被代理出去了,实现了不同VH逻辑的隔离。

关于自动注册ItemType,我们的做法是实现MutableList接口,内部组合一个普通的MutableList,对add,addAll,remove之类方法进行AOP处理,这些方法的执行的同时,自动检测或者注册ItemController,同时对于Adapter进行相应的Notify,这样子就可以实现一个轻量级的MVVM。

在这里,其实我们可以做很多事情,比如说代理出DiffUtil来进行自动Diff

此条目发表在转载文章分类目录,贴了标签。将固定链接加入收藏夹。

发表评论

电子邮件地址不会被公开。 必填项已用*标注