在RecyclerView中的ItemType很多时,Adapter的代码量也会随之不断增加,最近我一直在思考如何通过代码设计的方式来解决这种具象的业务问题,能否找到解决该类问题的通法?如何设计出一种模式,使得增删改一种ItemType时的成本降到最低?如何能够彻底解耦掉这些问题的核心大类——Adapter?
我们在使用RecyclerView的时候,总会遇到多项ItemType的场景。随着业务复杂度的增加,ItemType会越变越多,导致代码量越来越多,最终发展为“上帝类”,本文将从代码设计的角度来找到解决这类问题的通法。
业务场景还原
这里先通过Demo来还原一下真实的业务场景,后面便基于该场景进行问题的串联。假设现在只有三种ItemType,为了简单起见就分别对应A、B、C三种类型。
渲染数据
一般对应的数据也会有个字段来标识当前数据隶属于哪种type,首先定义实体ItemData。
1 | public class ItemData { |
通用模板
然后就是通用的代码模板,几乎所有的RyclerView.Adapter都会要写的数据传递和更新的逻辑。
1 | public class TargetAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { |
ItemType相关
定义ItemType
ItemType具体的值需要我们自己去定义,这里由于我们预先知道了一共三种type,所以预定义好如下常量。
1 | private static final int TYPE_A = 0; |
重写getItemViewType
重写该方法是为了告诉RecyclerView当前数据项对应哪中具体的ItemType。
1 |
|
定义对应type的ViewHolder
我们假设一个ViewHolder只对应单独的TextView,也只有一种普通的事件处理的需求。
1 | private static class AViewHolder extends RecyclerView.ViewHolder { |
其中B、C对应类似的代码,这里就不贴出来了。
重写onCreateViewHolder
重写该方法是为了给RecyclerView新建一个当前这种类型对应的ViewHolder实例。该方法的代码量直观感觉上不是太多,但却是后续进行代码解耦的难点所在。
1 |
|
重写onBindViewHolder
最后一步,重写该方法是为了处理ItemView的渲染和交互逻辑。该方法是Adapter逐渐臃肿的直接原因,这里为了简化Demo,已经将方法的处理逻辑简化掉了很多了,只包含一个TextView的渲染和点击交互,但即便如此该方法仍然占用接近40行的代码量。
1 |
|
代码总览
截止到目前,场景还原部分已经全部完成,此时的代码量已经有小200行了,而实际业务场景中的代码量只会多不会少。为了便于后续的描述,最后贴出当前Adapter全量的代码
1 | public class TargetAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> { |
开发痛点梳理
面向开发者而言,我们评判一个架构设计好坏的原则通常有两个维度——拓展性和维护性(当然还有健壮性,或称为鲁棒性,但该指标主要依赖于边界条件和异常情况的处理,它是所有编码的基本要求,这里不做赘述)。下面就基于这两个维度进行展开说明
拓展性
拓展性主要体现在基于当前的架构设计进行新功能增加时对原有代码所带来改动成本和波及范围方面,拓展性越差的设计所带来的改动成本和代码波及范围就越大,反之就越小。
下面结合一个具象的例子来体会当前的代码设计下的改动成本,假设现在添加一个新类型D,那我们需要如下改动:
首先需要添加一个新的ItemType常量
1
private static final int TYPE_D = 3;
然后需要getItemViewType方法添加
1
2case "typeD":
return TYPE_D;然后需要新的ViewHolder
1
2
3
4
5
6
7private static class DViewHolder extends RecyclerView.ViewHolder {
TextView textView;
DViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.item_text_d);
}
}然后onCreateViewHolder方法添加
1
2
3case TYPE_D:
itemView = inflater.inflate(R.layout.item_d, parent, false);
return new DViewHolder(itemView);最后onBindViewHolder方法添加
1
2
3
4
5
6
7
8
9
10case TYPE_D:
DViewHolder dViewHolder = (DViewHolder) holder;
dViewHolder.textView.setText(itemData.textD);
dViewHolder.textView.setOnClickListener(new View.OnClickListener() {
public void onClick(View v) {
Toast.makeText(context, "Click Item D", Toast.LENGTH_SHORT).show();
}
});
break;
至此,一个新类型的ViewHolder的逻辑已经全部添加完成,一共需要添加上述的5处的代码。
说下我个人最直观的感受——繁琐,非常繁琐!我新建一种类型的ViewHolder需要在一个几百行、甚至几千行代码的Adapter大类中去分别找到这5处不同的代码位置,然后逐一进行更改,缺一不可。万一我漏掉了一处怎么办?万一其中一处的代码我copy错了怎么办?都不能正常显示出新加项。而实际的开发场景中,我确实会很容易犯此类错误,以至于新添加一项ViewHolder就像背口诀一样逐一确认,防止漏加错加。
我希望的场景是怎样的,我希望新加ViewHolder时只需要添加一个类,从而低耦合;我希望这个类包含且只包含新加项的代码逻辑,从而高内聚。
维护性
维护性主要体现在基于当前的架构设计进行老功能修改时对原有代码所带来改动成本和波及范围方面,维护性越差的设计所带来的改动成本和代码波及范围就越大,反之就越小。此外,基于维护性而言编码最忌讳的就是“上帝类”的出现。
我们不需要也不希望一个类身兼多职、包罗万象。和企业用人不同的是,企业在资金有限的情况下,为减少开支尤为钟爱全能型人才,人越多使用成本越大;而编码方面,我们不怕小类多,我们怕大类不纯。权且不说什么单一原则、设计模式,我们就说最实在的,维护代码量太大的类,逻辑找起来揪心、改起来闹心。
比如现在要更改一下CViewHolder中的文本颜色逻辑,要根据数据进行动态设置。实际上只需要添加如下这一行代码即可,而我们定位到待添加代码的位置却要产生额外的检索时间消耗,即便我们的IDE很强大。
1 | cViewHolder.textView.setTextColor(itemData.isVip ? Color.YELLOW : Color.BLACK); |
明确目的
基于对上面开发痛点的梳理,现在总结下接下来最主要的目标——解耦Adapter,将各自ViewHolder的代码逻辑独立出去。
彻底解耦Adapter
对于解耦Adapter,由于之前我也有这方面的思考和尝试,也把ViewHolder从Adapter中进行抽离,但一直都不彻底,因为一个很核心的原因——ItemType,我在网上也看过很多的RecyclerView解耦方案,也同样有这个不足。
解耦从哪里开始
对于一个泛化意义的类,它开始转变复杂的最根本原因在于抽象语义的方法被业务场景具象化。所以我开始直接去找抽象语义的方法,就是如下的三个,而接下来解耦的主要工作就是这三个方法
1 | // 创建RecyclerView.ViewHolder实例 |
创建抽象基类
我们需要把多种类型ViewHolder里公有的逻辑抽象出来,而这些公有逻辑需要一个抽象基类作为逻辑载体。所以我们创建一个BaseViewHolder类。
1 | public class BaseViewHolder extends RecyclerView.ViewHolder { |
现在这个基类非常简单,只有一个onBindData的方法,用来让Adapter对ViewHolder进行数据绑定。有了该类之后,我便可以解耦出第一个方法onBindViewHolder了:
1 |
|
这一步我们相当于把具象化(A、B、C、D)的数据绑定逻辑抽象到基类里去了,然后只需要我们的具象ViewHolder继承该基类进行onBindData方法的重写即可,以AViewHolder为例:
1 | public class AViewHolder extends BaseViewHolder implements View.OnClickListener { |
与最开始我们在Adapter写的ViewHolder不同的是,ViewHolder不再仅仅是View的载体了,在当前这种模式下ViewHolder还包含数据绑定和事件处理逻辑。
同理,我们也对另外的B、C、D三种类型进行对应ViewHolder的创建。
而到了这一步,我们做的虽然并不是很多,但我们实际上已经完成了Adapter中很大部分的代码解耦了。因为上面也提到过,代码量占比最多的onBindViewHolder方法已经被我们holder.onBindData(itemData);
这一行代码搞定了。
选择,继续剥离还是适可而止
事先着重强调一点,选择没有明确对错,主要是看哪种更合适自己当前的业务场景。
- 如果只是简单的一两个ViewHolder的场景,哪怕连BaseViewHolder都没必要抽取;
- 如果类型稍微多一些,比如3-5种时,就要考虑抽取基类了,因为此时的代码量正常情况下已经足以让你的Adapter变得很臃肿、很庞大了;
- 如果类型更多,或者说明确知道后续还会有拓展,那仅仅到这一步还是远远不够的,因为Adapter里面还会包含各种type常量的声明,各种ViewHolder的创建。而且类型越多、对应的ViewHolder就越多,Adapter类就会越混乱、越庞大;
接下来就针对于继续剥离的分支进行讲述。
陷入僵局,蛇咬住了自己的尾巴
接下来,我们重点看onCreateViewHolder方法:
1 | BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType); |
入参是parrent和viewType,我们需要在该方法返回一个BaseViewHolder的实例给我们的RecyclerView。我们能不能把该方法再进行抽象?那我们又如何去根据viewType来实例化我们具象的BaseViewHolder?上面我们之所以能把onBindViewHolder的逻辑往基类里面抽取,是因为有具象的ViewHolder实例,我们可以通过具象的子ViewHolder实例来调用自己重写的逻辑块。但这里不一样,因为这里是创建BaseViewHolder的地方,也就是说此时我们根本没有ViewHolder实例,所以我们无法将这个创建逻辑通过BaseViewHolder进行抽象表征。
RecyclerView:我告诉你viewType,你给我创建一个ViewHolder实例。
Adapter:我可以创建,但我不知道你要哪种实例。你给我ViewHolder,我得问它。
RecyclerView:你不给我创建ViewHolder,我怎么给你?
Adapter:你不给我ViewHolder,我怎么创建?
我们现在把具象的ViewHolder作为具象的逻辑载体;而实例化这些具象ViewHolder对象也属于具象的逻辑。这就好像是蛇咬住了自己的尾巴。
破局,再向上抽象一层
ViewHolder能具象解决的是视图渲染和用户交互,不能解决的是自身实例的创建。所以我打算在ViewHolder的基础上再向上抽象一层,专门为了解决ViewHolder的实例化问题。而在Java中这种专门为了创建实例的方法,我们一般称之为工厂方法,所以我就打算BaseViewHolder里新建一个工厂类,同时各个子ViewHolder里都有对应的Factory来继承它,如下:
1 | public interface Factory { |
抽象出来的工厂接口中有个getItemType方法,用以告诉Adapter当前ViewHolder对应的ItemType。
1 |
|
在Adapter中就用当前项的ItemType来跟每个Factory返回的进行比对,看是否是当前ViewHolder能识别的,进而决定调用哪个Factory类的实例方法。
对应的,我们现在就可以把Adapter中创建ViewHolder的代码迁移到子类中去了,如下为AViewHolder的内部类:
1 | public static class Factory implements BaseViewHolder.Factory { |
同样的抽离方式,我们把另外几个类型(B、C、D)都添加如上的Factory类。
最难的问题,还是来了
onCreateViewHolder方法已经解耦出去了,但马上便遇到另一个问题,每个Factory内都要返回一个ItemType,而返回的ItemType实际上是不能重复的,因为Adapter创建ViewHolder是根据ItemType来区分的。现在我们的ViewHolder虽然从Adapter抽离出来了,但是ViewHolder之间却还有这种相互制约的关系。我每新添加一个新类型的ViewHolder时都还需要看一下之前的ViewHolder里对应返回的ItemType以免冲突,这种方式着实很不方便,这个问题记为①先暂且放下。
除此之外我们还有第②个问题,那边是此时的Adapter还有最有一个未解耦也是最难解耦的方法——getItemType,我们再看下该方法的代码:
1 |
|
我们可以看到这里的TYPE_A、TYPE_B等常量不仅在这里使用,而且还要跟抽取的Factory中返回的getItemType要对应上。
而这两个问题都是ItemType相关的逻辑,为了更加透彻的了解ItemType真实用意,我们来看下源码的解释:
1 | /** |
大意是说,它跟ListView里的getItemType不同的是,这里的ItemType可以是不连续的,但必须是唯一的,可以考虑使用id作为唯一标识。确实是很不错的建议,这样便能解决第一个ItemType冲突的问题。但是第二个问题依然解决不了,因为我还需要去做数据层的ItemData#type
到View层的ItemType的关系映射。
很坦白的说,我在这个问题上思考了很久,我尝试在网上去寻找一些框架、源码、解决方案,但最终一无所获。
寻根溯源,从根本上解决
我开始进行了很长一段时间的思维脑洞,促使我最终开始慢慢理清问题的脉络
Adapter为什么要有ItemType的概念,用来干什么?
我们的RecyclerView有复用View的机制,同时RecyclerView中还可以包含不同种的类型的View,那在复用的时候,它需要一个表明当前数据项对应哪中类型View的标识,而这个标识便是ItemType。
ItemType在当前场景下的整条链路关系是怎样的?
- 后端返回的是ItemData数据实体,该实体的type字段用以标识当前的类型
- 我们重写Adapter的getItemType方法,在这里做一层后端type字段到Adapter中ItemType的映射关系,然后告诉Adapter当前的ItemType
- Adapter用我们告诉它的ItemType来查找对应缓存的子ViewHolder是否足够使用,够则复用,不够则创建,而这里的创建便是onCreateViewHolder方法
- 无论创建还是复用,得到这个ViewHolder对象之后,都会再调用onBindViewHolder方法让我们进行页面渲染和事件绑定
据上,ItemType的根本作用就是唯一标识ViewHolder的类型,而实际上ItemData中的type已经是为了标识而存在的,我们重写Adapter的getItemType方法只是为了将type字段的字符串标识值翻译成int标识值以适配Adapter中关于ItemType的约定。既然如此,那我们为什么不把这层映射关系通过编码的方式实现来取代这种手动一对一映射的方式呢?
于是,便有了下一个版本的Factory类:
1 | public interface Factory { |
现在我们的Factory不在依赖于一个具象写死且无含义的ItemType,而是对应一个带有语义的字符串,而且上面也提到,后端返回给我们的type其实就是一个字符串的值(如”typeA”,”typeB”…),这样我们就能解耦掉Adapter中的最后一个方法getItmeType了,现在的Adapter变动的代码如下:
1 |
|
- 这里之所以要记录数据type和ItemType的映射关系,是因为我们需要在getItemViewType方法中知道
type => ItemType
的映射关系,进而将我们语义理解的type转为RecyclerView能够识别的ItemType。 - 同时还做了针对注册同名type情况的异常检查,如果两个Factory的type是相同的,那么Adapter将在构造方法中把这种错误尽可能早的检查并暴露出来。
与此对应的,子ViewHolder的Factory也需要做出细微的调整:
1 | public static class Factory implements BaseViewHolder.Factory { |
渲染问题刚解决,通信问题又出现
前面花费了很大的精力将ViewHolder从Adapter中解耦出来,已经完全解决了ViewHolder中View的渲染问题,但接踵而至的便是通信问题,比如,我在点击ViewHolder中的某一个子View的时候,我需要对应的Activity去做一些一些逻辑处理,或者说我需要在ViewHolder中取获取Activity的内部状态(如获取成员变量)。
注:我看过很多在ViewHolder等组件中直接通过将context强转成当前页面的Activity,然后调用其方法的写法,如果页面、业务逻辑不是复杂这个写法尚可,一旦变得复杂,这种写法是很不推荐的。业务主体类不要被其子组件显式调用,如果一定需要调用,那就替换为通过Callback来隐式调用的方式。否则就会出现,A类依赖B,B类依赖A这种相互依赖的关系,会让逻辑变得异常混乱;而且如果子组件要被复用时,代码中的这种强转+显式调用的代码将成为抽取抽象逻辑时很大的一个坑。
该问题出现的根本原因,其实并不是我们解耦的ViewHolder导致的,对于一个普通的Activity => Adapter => ViewHolder这样的调用链而言也一样会存在该问题。只不过我们的设计结构在这条链路上多添加了一个Factory的节点而已,添加之后的链路变成了Activity => Adapter => Factory => ViewHolder。所以我们需要一个上下文类,来穿透这条调用链路,然后ViewHolder中触发的事件便可以通过该上下文类将事件透传到Activity中去。这样描述起来可能会写生硬,下面直接看代码
1 | // 定义上下文类 |
这样一整条链路便串联起来了,ViewHolder与Activity的通信就直接通过HolderContext即可完成,而不需要Adapter和Factory的任何额外处理。这条链路的传递看起来很长、很复杂,但其实这层传递只需要写一遍,后续拓展如果再需要添加一个BViewHolder的事件,只需要在HolderContext中添加一个onClickB方法,并在Activity中实现即可。
端上结构设计已解耦,数据再解耦
到了这里,基于结构设计的维度,Adapter中已经不再是与业务耦合的类了,但是当前类还包含ItemData这个数据类,我想把该类再进行一层抽象,从而让Adapter完全与业务无关,因为只有使得它完全与业务无关,我才能进而对于该类进行抽象,做成了抽象也就意味着通用。
接下来把ItemData替换为如下的IType接口:
1 | public interface IType { |
接下来把Adapetr中出现ItemData的地方全部替换为IType即可
收官,最终的模样
前面都是以遇到的问题为线索逐一进行解决,下面可以看下Adapter最终的代码结构:
1 | public class TargetAdapter extends RecyclerView.Adapter<BaseViewHolder> { |
到了这一步,Adapter的代码量已经精简到原来的一半左右,并且不再冗余任何具象业务逻辑的代码,这也就意味着该类不再是基于当前业务维度的类,而是一个解耦掉ViewHolder且从具象业务逻辑中泛化出来的抽象类,所以对于任何需要添加多个ItemType的Recycler.Adapter场景均可基于该类进行添加。
痛点不再痛
回顾以上提到的两个痛点,维护性和拓展性,现在就开始基于本文最开始业务场景进行类型E这种Type进行拓展,来看下对应的代码改动:
1. 添加一个独立的EViewHolder类
1 | public class EViewHolder extends BaseViewHolder implements View.OnClickListener { |
2. 注册EViewHolder.Factory
1 | adapter = new BaseAdapter(holderContext, Arrays.asList( |
是的,仅此两步就已经结束了!
回顾最开始提到的低耦合高内聚的原则,再对比一下在解耦前后来新添加EViewHolder对应的代码改动。
关于耦合,整个Activity与EViewHolder唯一耦合的地方只有一处绑定关系的代码——new EViewHolder.Factory()
,而如果业务上有临时变更不需要展示EViewHolder项,只需要注释掉该行代码即可(Adapter需要添加通用的边界处理逻辑,不赘述)。
关于内聚,对比最开始的结构,每种类型的ViewHolder代码都是散落在Adapter中的各个地方:ItemType的声明写在Adapter的常量中,ViewHolder的创建写在了onCreateViewHolder方法中,ViewHolder的渲染写在了onBindViewHolder中,后端数据的type到ItemType的映射写在了getItemType中。而解耦之后,这里所有与ItemType有关的具象业务逻辑全部都内聚在EViewHolder.java
一个文件中了。
至此,Adapter解耦的一整套链路全部已经打通了。
总结
由于本文整篇较长,所以这里再做一下前面核心脉络的回顾:
- 开发痛点是RecyclerView.Adapter的拓展性和维护性不够好,而基于这两者再进行分析,可归纳为一点——Adapter与ViewHolder之间的耦合性太强,所以明确接下来的目的就是解耦Adapter与ViewHolder
- 接下来便开始解耦Adapter中最关键的三个方法:onBindViewHolder、onCreateViewHolder、getItemType
- 解耦onBindViewHolder通过抽象BaseViewHolder的方式解决
- 解耦onCreateViewHolder通过ViewHolder中新建Factory方式解决
- 解耦getItemType通过将最开始的Factory中的getItemType返回具象ItemType的方式替换为getType返回数据层面的type的方式解决
- 然后进行数据层的抽象,将ItemData业务实体替换为Adapter关注的IType抽象接口
- 最后进行业务层的业务层的彻底剥离,将所有业务无关项进行Base类统一抽象