Jul
26
最近花费了两周的时间对RecyclerView进行了再解耦和结耦封装,以达到以更优雅的方式满足业务需求,并且能方便的应用到之后的业务中去。最终目标是对于不复杂的列表,可以在数行代码内搞定列表配置与数据绑定;对于较为复杂的列表,可以以非常清晰的代码完成工作。
关键痛点
之所以做这么一件事儿,是我们注意到之前的业务中RecyclerView相关的代码之乱,令人无法忍受。至于怎么乱呢:最痛的是大量代码集中在adapter中。整理一下,adapter中包含了些啥:
1. 数据,由于在列表滑动中需要持续读取各个位置的数据,所以往往数据是由adapter直接持有的,官方自带的几个adapter也是这样做的。持有了数据的同时,当然,对数据增减移动,特殊列表操作相关的计算代码也在其中了。
2. viewHolder,由于每一个类型的item就会跟上一种viewholder并且写出一个继承了类,因此adapter末尾往往堆积了好几个viewholder的内部子类。
3. create/bind,本来,这当然是交给adapter做的没问题,但由于出现了多种type的item,因此这两个方法类会通过itemType和viewholder的instanceof判断出多种枚举并各自编写逻辑或者胡乱的复用逻辑,导致这两个方法引出的代码相当繁杂。
4. item操作:如果item上有一些界面交互等,需要设置等listener等处理方法也会写在adapter中,同时反馈处理到数据源。
由以上代码的描述,我们同时也可以简短的整理出我们面临的关键痛点:
1. 数据操作和界面操作代码混杂
2. viewHolder编写繁琐
3. 手动区分itemType代码不够清晰
4. 界面与数据的反向交互处理方案不确定
5. 以上问题均出现在一个文件中!
尝试解决
已知以上问题了,我们就开始尝试解决吧。
数据
这也是最大的问题,数据和界面相关的代码混合在了一个adapter中,大家都这样写的原因是adapter提供的接口:
额,两个界面相关,两个数据相关,我们当然写了一起啦。不过再看看iOS中tableView的同样相似的接口UITableViewDelegate, UITableViewDataSource。嗯,原来iOS确实是把数据源的代理接口和界面的代理接口区分开来啦。那么我们就参考参考这样做吧。回到安卓的adapter,显然,我们要做的是把数据相关的接口整理到新的接口中去。因此如下设计了DataSource接口:
除了获取数据类型和获取数据数量,当然,我们肯定还需要通过position获得当前的数据具体对象,这样就足够了。之后,adapter只要持有这么一个抽象的dataSource,就可以完整的获得数据所需的全部信息了。
同样,除了adapter会调用dataSource中的数据,dataSource要先主动告知adapter我更新了什么数据,不过仔细想想,只需要通过抽象的adapter提供的notifyDataSetChanged和notifyItemXXX那一套方法进行通知就可以达到目的了。因此dataSource也只需要持有一个抽象的adapter,就可以完成对adapter的操作。这样完成了一个数据接口和界面接口的操作闭环,同时也非常干净的分离了代码。
item类型区分
这应该是第二大问题,即多个类型的adapter绑定操作混在了同一对create/bind方法中导致代码臃肿混乱。当然,解决方法也很明显了——按类型把每个类型的create/bind抽象到一个单独的接口中,并且按照item的type注册接口就可以了。因此同样的,我们编写接口:
嗯,我们就需要这么两个方法就可以完成对一类item的描述,其中除了限定viewHolder的类型,同时也限定了data的数据类型——毕竟通常情况下就是一个类型有一类特定的数据。最后,由原生adapter持有这一些ItemAdapter,通过一个map(实际使用了一个SparseArray)来储存这些itemadapter,当然key就是它对应的type了。最后根据datasource中的type运算,把具体的操作,分发到ItemAdapter就可以。
到这里为止,我们把adapter中最主要的四个接口方法全覆盖完毕了。其中getItemCount和getItemViewType交给了DataSource,onCreateViewHolder和onBindViewHolder分发给了复数个ItemAdapter中,原生adapter就像一个耦合器一样把这些分散的代码结合在一起。这样adapter就完全拆解开了。
ViewHolder优化
本来,ViewHolder和adapter在一起,和adapter一起臃肿,然后随着上一点改进之后,每一个ViewHolder将会和对应使用的ItemAdapter放在一起,这样已经能很大程度上解决问题了。不过我们还说想,对于简单使用的ViewHolder,它的本质就是持有一下需要进行操作的view的引用以避免每次绑定都运行findViewById之类的操作带来的开销。既然只是绑定一些数据,我们并不需要每次都继承一个下来把绑定的数据当作属性啊。那优化方案也很显然了——用一个map缓存引用不就得了。
我们还是使用一个map(SparseArray)来存储view的引用,其中key是资源的id,合情合理令人信服。
这样一个holder,我们可以在ItemAdapter对他进行构造的时候就顺带传入需要绑定的界面元素的id完成hold。更有甚者,我们可以考虑直接将数据和界面通过某种方式绑定起来,类似于mvvm的理念,可以就可以不关注ItemAdapter细节,甚至不用编写itemAdapter来完成item了。那是后文中的内容。
其它元素
继续对上文中的几个元素进行优化和抽象,我们继续发现了几个可以抽象的点。
1. 数据,从数据源到界面显示的数据,很显然,包含了两个数据的转换过程;同时,界面上旧数据变幻为新数据,又出现了一个数据渲染的过程。这两个过程均有可抽象的地方。其中数据变幻过程,我们引入了Filter和Sorter两个概念和接口,对数据进行过滤和排序。另外,在显示数据更新的地方,引入DataRender概念,实现包涵各自特点或者倾向的动画或静态更新数据的过程。
2. Holder,它的核心内容是事上就是储存和描述item界面本身的对象,而实际使用中,item会有一些可共用的特性可以进行抽象。因此封装出HolderBehavior来描述Holder的特性,并且可以以可插拔的方式加入继承于XGViewHolder的holder中。
3. HeaderFooter,讲道理每一个数据源的数据对应一个item才是合理的,不过实际使用中我们往往会加入Header和Footer来完成一些非常重要的功能。因此在adapter中,提供了添加他们的方法。当然,HeaderFooter在adapter看来就是拥有特殊type的ItemAdapter而已,不过考虑到并没有数据对应,所以需要特意为他们两作数据的offset,这是adapter中惟一没有按设计的规则出牌的地方。
上层封装
基础构架完成了,我们已经把原本的adapter-viewHolder组合拆解成为dataSource(filter-sorter-dataRender)-adapter-itemAdapter-viewHolder(holderBehavior)这样分散开来的对象和接口了,那么为了完成需求以实现我们一起曾经以较为简单的代码就可以完成的东西,我们接下来需要做一些上层的封装来填充这个构架。
1. ViewHolder : 首先,我们完成了BindViewHolder来使得更加方便,不用继承的完成ViewHolder的绑定。其次,我们希望有一些与定义预定义好的元素不用手动设置layout就可以作为根元素直接使用,因此有了ButtonViewHolder,TextViewHolder。同时,我们有holder的表现特性StripedHolderBehavior即条纹状的holder和SelectableHolderBehavior可选状的holder,为了便于继承性质的使用,封装这两个特性到SimpleViewHolder。以上应该可以覆盖从简单绑定到深度定制的全部item绑定场景了。
2. DataSource : 数据源只是一个抽象的接口,首先我们需要实现一个实际的基类SimpleDataSource<DATA>,它实现了上文所说的数据过滤排序渲染的整个过程,并且绑定固定的数据结构DATA。LoadableDataSource<DATA>用于可以向下持续加载的数据源。CollapsibleDataSource<DATA>用于可以折叠展开尾部数据的数据源。SectionDataSource<SectionItem, SubItem>则是非常重要的,可分栏的两级列表,当然,也需要指定两种数据结构。MenuDataSource<SectionItem, SubItem>则是section的更深层次的应用,完成的是类似于多级菜单的交互。以上5个数据源由浅至深的覆盖了之前业务中涉及到的所有列表交互场景,同时由于基类中有dataRender可以自动计算完成动画,因此可以非常方面的实现以前需要复杂计算才能完成的功能。
3. 其它元件。为了全面覆盖常见的功能,还有一些其它的辅助类。DividerDecoration,是一个简单实现分隔栏的Decoration。PinnedSectionItemDecoration,是专门用于section数据源或者使用了section为type作为类型的数据源,可以实现浮动标题固定的效果。
4. recyclerView上层封装 : 前面的那些其实都是adapter内部和recyclerView的可插拔组件的封装,recyclerView本身也有必要进行一些可选性的封装。封装SimpleRecyclerView,完成了常见的配置,如设置纵向线性布局,设置adapter等,并且把adapter中重要的方法暴露出来以方便使用。再往上封装了RefreshRecyclerView,即可下拉刷新的列表,其继承于SwipeRefreshLayout,同时还考虑接入加载/空白/错误状态的提示。
结束
以上内容便是整个RecyclerView进行再解耦和结耦封装的过程。当然,封装出的结果,大家看到了,虽然拆封解耦清晰,不过为了便于使用,又重新可选性的结耦在了一起。并且说实话,概念确实复杂了很多,不是很友好。所以在其上层,我们还封装了一层builder来实现我们的最终目的——分分钟构建list。后续文章再提builder的设计。
关键痛点
之所以做这么一件事儿,是我们注意到之前的业务中RecyclerView相关的代码之乱,令人无法忍受。至于怎么乱呢:最痛的是大量代码集中在adapter中。整理一下,adapter中包含了些啥:
1. 数据,由于在列表滑动中需要持续读取各个位置的数据,所以往往数据是由adapter直接持有的,官方自带的几个adapter也是这样做的。持有了数据的同时,当然,对数据增减移动,特殊列表操作相关的计算代码也在其中了。
2. viewHolder,由于每一个类型的item就会跟上一种viewholder并且写出一个继承了类,因此adapter末尾往往堆积了好几个viewholder的内部子类。
3. create/bind,本来,这当然是交给adapter做的没问题,但由于出现了多种type的item,因此这两个方法类会通过itemType和viewholder的instanceof判断出多种枚举并各自编写逻辑或者胡乱的复用逻辑,导致这两个方法引出的代码相当繁杂。
4. item操作:如果item上有一些界面交互等,需要设置等listener等处理方法也会写在adapter中,同时反馈处理到数据源。
由以上代码的描述,我们同时也可以简短的整理出我们面临的关键痛点:
1. 数据操作和界面操作代码混杂
2. viewHolder编写繁琐
3. 手动区分itemType代码不够清晰
4. 界面与数据的反向交互处理方案不确定
5. 以上问题均出现在一个文件中!
尝试解决
已知以上问题了,我们就开始尝试解决吧。
数据
这也是最大的问题,数据和界面相关的代码混合在了一个adapter中,大家都这样写的原因是adapter提供的接口:
public ViewHolder onCreateViewHolder(ViewGroup parent, int viewType);
public void onBindViewHolder(ViewHolder holder, int position);
public int getItemViewType(int position);
public int getItemCount();
额,两个界面相关,两个数据相关,我们当然写了一起啦。不过再看看iOS中tableView的同样相似的接口UITableViewDelegate, UITableViewDataSource。嗯,原来iOS确实是把数据源的代理接口和界面的代理接口区分开来啦。那么我们就参考参考这样做吧。回到安卓的adapter,显然,我们要做的是把数据相关的接口整理到新的接口中去。因此如下设计了DataSource接口:
public interface DataSource {
int getDataType(int position);
int getDataCount();
Object getData(int position);
}
除了获取数据类型和获取数据数量,当然,我们肯定还需要通过position获得当前的数据具体对象,这样就足够了。之后,adapter只要持有这么一个抽象的dataSource,就可以完整的获得数据所需的全部信息了。
同样,除了adapter会调用dataSource中的数据,dataSource要先主动告知adapter我更新了什么数据,不过仔细想想,只需要通过抽象的adapter提供的notifyDataSetChanged和notifyItemXXX那一套方法进行通知就可以达到目的了。因此dataSource也只需要持有一个抽象的adapter,就可以完成对adapter的操作。这样完成了一个数据接口和界面接口的操作闭环,同时也非常干净的分离了代码。
item类型区分
这应该是第二大问题,即多个类型的adapter绑定操作混在了同一对create/bind方法中导致代码臃肿混乱。当然,解决方法也很明显了——按类型把每个类型的create/bind抽象到一个单独的接口中,并且按照item的type注册接口就可以了。因此同样的,我们编写接口:
public interface ItemAdapter<VH extends ViewHolder, DATA> {
void onBindViewHolder(VH holder, DATA data);
VH onCreateViewHolder(ViewGroup parent, int viewType);
}
嗯,我们就需要这么两个方法就可以完成对一类item的描述,其中除了限定viewHolder的类型,同时也限定了data的数据类型——毕竟通常情况下就是一个类型有一类特定的数据。最后,由原生adapter持有这一些ItemAdapter,通过一个map(实际使用了一个SparseArray)来储存这些itemadapter,当然key就是它对应的type了。最后根据datasource中的type运算,把具体的操作,分发到ItemAdapter就可以。
到这里为止,我们把adapter中最主要的四个接口方法全覆盖完毕了。其中getItemCount和getItemViewType交给了DataSource,onCreateViewHolder和onBindViewHolder分发给了复数个ItemAdapter中,原生adapter就像一个耦合器一样把这些分散的代码结合在一起。这样adapter就完全拆解开了。
ViewHolder优化
本来,ViewHolder和adapter在一起,和adapter一起臃肿,然后随着上一点改进之后,每一个ViewHolder将会和对应使用的ItemAdapter放在一起,这样已经能很大程度上解决问题了。不过我们还说想,对于简单使用的ViewHolder,它的本质就是持有一下需要进行操作的view的引用以避免每次绑定都运行findViewById之类的操作带来的开销。既然只是绑定一些数据,我们并不需要每次都继承一个下来把绑定的数据当作属性啊。那优化方案也很显然了——用一个map缓存引用不就得了。
我们还是使用一个map(SparseArray)来存储view的引用,其中key是资源的id,合情合理令人信服。
public class BindViewHolder extends XGAdapter.ViewHolder {
private SparseArray<View> viewHolder = new SparseArray<>();
public BindViewHolder(View itemView, int... ids) {
super(itemView);
for (int id : ids) {
viewHolder.put(id, itemView.findViewById(id));
}
}
public <T> T get(int id) {
return (T) viewHolder.get(id);
}
public <T> T get(int id, Class<T> c) {
return (T) viewHolder.get(id);
}
}
这样一个holder,我们可以在ItemAdapter对他进行构造的时候就顺带传入需要绑定的界面元素的id完成hold。更有甚者,我们可以考虑直接将数据和界面通过某种方式绑定起来,类似于mvvm的理念,可以就可以不关注ItemAdapter细节,甚至不用编写itemAdapter来完成item了。那是后文中的内容。
其它元素
继续对上文中的几个元素进行优化和抽象,我们继续发现了几个可以抽象的点。
1. 数据,从数据源到界面显示的数据,很显然,包含了两个数据的转换过程;同时,界面上旧数据变幻为新数据,又出现了一个数据渲染的过程。这两个过程均有可抽象的地方。其中数据变幻过程,我们引入了Filter和Sorter两个概念和接口,对数据进行过滤和排序。另外,在显示数据更新的地方,引入DataRender概念,实现包涵各自特点或者倾向的动画或静态更新数据的过程。
2. Holder,它的核心内容是事上就是储存和描述item界面本身的对象,而实际使用中,item会有一些可共用的特性可以进行抽象。因此封装出HolderBehavior来描述Holder的特性,并且可以以可插拔的方式加入继承于XGViewHolder的holder中。
3. HeaderFooter,讲道理每一个数据源的数据对应一个item才是合理的,不过实际使用中我们往往会加入Header和Footer来完成一些非常重要的功能。因此在adapter中,提供了添加他们的方法。当然,HeaderFooter在adapter看来就是拥有特殊type的ItemAdapter而已,不过考虑到并没有数据对应,所以需要特意为他们两作数据的offset,这是adapter中惟一没有按设计的规则出牌的地方。
上层封装
基础构架完成了,我们已经把原本的adapter-viewHolder组合拆解成为dataSource(filter-sorter-dataRender)-adapter-itemAdapter-viewHolder(holderBehavior)这样分散开来的对象和接口了,那么为了完成需求以实现我们一起曾经以较为简单的代码就可以完成的东西,我们接下来需要做一些上层的封装来填充这个构架。
1. ViewHolder : 首先,我们完成了BindViewHolder来使得更加方便,不用继承的完成ViewHolder的绑定。其次,我们希望有一些与定义预定义好的元素不用手动设置layout就可以作为根元素直接使用,因此有了ButtonViewHolder,TextViewHolder。同时,我们有holder的表现特性StripedHolderBehavior即条纹状的holder和SelectableHolderBehavior可选状的holder,为了便于继承性质的使用,封装这两个特性到SimpleViewHolder。以上应该可以覆盖从简单绑定到深度定制的全部item绑定场景了。
2. DataSource : 数据源只是一个抽象的接口,首先我们需要实现一个实际的基类SimpleDataSource<DATA>,它实现了上文所说的数据过滤排序渲染的整个过程,并且绑定固定的数据结构DATA。LoadableDataSource<DATA>用于可以向下持续加载的数据源。CollapsibleDataSource<DATA>用于可以折叠展开尾部数据的数据源。SectionDataSource<SectionItem, SubItem>则是非常重要的,可分栏的两级列表,当然,也需要指定两种数据结构。MenuDataSource<SectionItem, SubItem>则是section的更深层次的应用,完成的是类似于多级菜单的交互。以上5个数据源由浅至深的覆盖了之前业务中涉及到的所有列表交互场景,同时由于基类中有dataRender可以自动计算完成动画,因此可以非常方面的实现以前需要复杂计算才能完成的功能。
3. 其它元件。为了全面覆盖常见的功能,还有一些其它的辅助类。DividerDecoration,是一个简单实现分隔栏的Decoration。PinnedSectionItemDecoration,是专门用于section数据源或者使用了section为type作为类型的数据源,可以实现浮动标题固定的效果。
4. recyclerView上层封装 : 前面的那些其实都是adapter内部和recyclerView的可插拔组件的封装,recyclerView本身也有必要进行一些可选性的封装。封装SimpleRecyclerView,完成了常见的配置,如设置纵向线性布局,设置adapter等,并且把adapter中重要的方法暴露出来以方便使用。再往上封装了RefreshRecyclerView,即可下拉刷新的列表,其继承于SwipeRefreshLayout,同时还考虑接入加载/空白/错误状态的提示。
结束
以上内容便是整个RecyclerView进行再解耦和结耦封装的过程。当然,封装出的结果,大家看到了,虽然拆封解耦清晰,不过为了便于使用,又重新可选性的结耦在了一起。并且说实话,概念确实复杂了很多,不是很友好。所以在其上层,我们还封装了一层builder来实现我们的最终目的——分分钟构建list。后续文章再提builder的设计。