0%

用 SugarAdapter 简化复杂列表的实现

前言

在一个 App 中 Feed 流类型的页面非常依赖 RecyclerView 的使用,有大量的列表。而列表中卡片(即列表中的 Item,以下统称卡片)有很多种样式,并且不同的样式对应的不同的业务需求,使得 Adapter 的实现非常的爆炸,因此需要一种合适的方式去实现 Adapter。

本文结合日常开发的场景,简单分析一下,使用常规手法如何最大的程度的优化 RecyclerView 的 Adapter 的实现。最后分析一下 SugarAdapter,看看使用它是如何解决常规实现痛点的。

SugarAdapter 是什么 ?

SugarAdapter, Make RecyclerView.Adapter Great Again!

常规的 Adapter 有什么痛点

面对一个有上百种卡片样式的 RecyclerView ,简单粗暴的做法可能就是把所有可能需要显示的 UI 元素怼在一个 xml 布局文件里,然后在 Adapter 里根据数据对 view 做 visible/invisible/gone 及 setXXX 的操作,这无疑是最糟糕的做法,代码会爆炸,性能更是堪忧,基本上就无法维护了。

换个思路,本质上每一种卡片样式其实就是一种 ViewHolder(onCreateViewHolder 返回值是 Holder,至于为啥是 Holder,而不是直接返回 View,用过上古神器 ListView 的同学一定知道原因的) ,那么面对这么多的卡片样式,写一大堆 ViewHolder 就可以了,事实上在大部分时候,我们确实是这么做的,新的需求来了,添加一个新的 ViewHolder 或者是继承一个已有的 ViewHolder 做一下扩展就可以了。

下面用最简单的代码,描述以下上述实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
public class NormalAdapter extends RecyclerView.Adapter<NormalAdapter.MyHolder> {

private List<String> datas = new ArrayList<>();
private static final int VIEW_TYPE_A = 0;
private static final int VIEW_TYPE_B = 1;

public NormalAdapter(List<String> datas) {
this.datas = datas;
}

@NonNull
@Override
public MyHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
if (viewType == VIEW_TYPE_A) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_type_a, parent, false);
return new HolderA(view);
} else if (viewType == VIEW_TYPE_B) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_type_b, parent, false);
return new HolderB(view);
} else {
// more view_type stuff
return null;
}
}

@Override
public void onBindViewHolder(@NonNull MyHolder holder, int position) {
int viewType = getItemViewType(position);
if (viewType == VIEW_TYPE_A) {
// data-to-ui stuff
} else {
// data-to-ui stuff
}
}

@Override
public int getItemViewType(int position) {
if (position % 2 == 0) {
return VIEW_TYPE_A;
} else {
return VIEW_TYPE_B;
}
}

@Override
public int getItemCount() {
return datas.size();
}

public class MyHolder extends RecyclerView.ViewHolder {

public MyHolder(@NonNull View itemView) {
super(itemView);
}
}

public class HolderA extends MyHolder {
public HolderA(@NonNull View itemView) {
super(itemView);
}
}

public class HolderB extends MyHolder {
public HolderB(@NonNull View itemView) {
super(itemView);
}
}

}

如上,是 RecyclerView.Adapter 实现的常规操作。ViewHolderA 和 ViewHolderB 对应不同的卡片样式。如果有新的样式,继续添加即可。但是每添加一个新的 ViewHolder ,就需要改三个方法(getItemViewType,onCreateViewHolder,onBindViewHolder).当然,可以通过工厂方法等设计模式,修改具体创建 ViewHolder 的方法,但本质上要修改的地方不会变少。

ViewHolder 的本质

1
2
3
4
5
6
7
8
9
10
11
@Override
public int getItemViewType(int position) {
Object object = datas.get(position);
if (object instanceof ModelA) {
return VIEW_TYPE_A;
} else if (object instanceof ModelB) {
return VIEW_TYPE_B;
}else {
return -1;
}
}

由于 RecyclerView 没有提供原生的方法用来为列表添加 Header 和 Footer ,因此有在 position 为 0 和列表末尾时,返回特定的 viewtype 来添加 Header 和 Footer 也是一种常规用法。

假设,我们现在需要添加 ViewHolderC,我们需要以下几个步骤:

  • 从 getItemViewType 开始,确定 ViewType 的值。一般来说,会根据数据类型确定这个值。当然,这里不一定通过 instanceof 实现,也可以通过数据中特定字段实现,但本质是相同的,数据决定 viewType 类型。
  • 然后根据这个 ViewType 在 onCreateViewHolder 方法中确定其对应的 ViewHolder 的类型,更进一步来说是确定了要 inflate 的 xml 资源文件,通过这个 xml 对应的 view 创建 ViewHolder。
  • 最后,在 onBindViewHolder ,我们确定了最终需要使用的 ViewHolder 和 Model(数据类型),这里可能需要强转以下,但是只要以上两步没有错,那么这个强转是没有任何问题。

但是,还是回到一开始要面对的问题,面对上百种的卡片样式,这种方式又变得不太友好,每次添加一个新的 ViewHolder,需要修改已有代码,无疑会带来不可预料的 bug。

那么我们改如何面对这种情况呢? 一种可行的解决方式 彻底解耦 RecyclerView.Adapter,这篇文章中提到的解决方式其实和接下来要分析的 SugarAdapter 的思路大致相同,只不过 SugarAdapter 采用注解的方式,免去了自建工厂类的麻烦,实现更加的优雅,因此使用起来也更加的方便。

SugarAdapter 如何解决了这些痛点

关于如何使用 SugarAdapter 及它可以覆盖哪些场景,这里就不再展开了,参考其 README 文档,就可以非常简单而又快速的上手了,非常好用,墙裂推荐 !!!

Layout & ViewHolder

首先看一下如何创建一个 ViewHolder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Layout(R.layout.layout_bar)
public final class BarHolder extends SugarHolder<Bar> {

@Id(R.id.text)
public AppCompatTextView mTextView;


public BarHolder(@NonNull View view) {
super(view);
}

@Override
protected void onBindData(@NonNull Bar bar) {
mTextView.setText(bar.getText());
}
}

看以看到,创建一个 ViewHolder 只需要做 4 件事情

  • Layout 注解提供 ViewHolder 对应的 xml 布局文件
  • 继承 SugarHolder 并指定泛型的具体类型
  • 为用到的 View 加上 @ID 的注解,框架会自动帮你 fvb(当然,你也可以自己在构造函数里写 fvb;如果你在用 kotlin,忽略这条)
  • 实现 onBindData 方法,完成数据和 UI 的绑定。

上面的注解在编译期,会生成如下的文件(关于 SugarAdapter 注解处理器如何解析注解及生成文件,可以参考SugarAdapter 注解处理器分析

ContainerDelegateImpl.java

在 build/generated/source/apt/debug/com/zhihu/android/sugaradapter/ 目录下 (生成的代码是 ViewHolder 类名相关,不一定和这里完全相同,但方法体及实现是雷同的)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public final class ContainerDelegateImpl implements ContainerDelegate {
private Map<Class<? extends SugarHolder>, Integer> mLayoutResMap;
private Map<Class<? extends SugarHolder>, Class> mDataClassMap;

public ContainerDelegateImpl() {
mLayoutResMap = new HashMap<>();
mDataClassMap = new HashMap<>();

mLayoutResMap.put(com.zhihu.android.sugaradapterdemo.holder.BarHolder.class, com.zhihu.android.sugaradapterdemo.R.layout.layout_bar);
mDataClassMap.put(com.zhihu.android.sugaradapterdemo.holder.BarHolder.class, com.zhihu.android.sugaradapterdemo.item.Bar.class);
}

@NonNull
public Map<Class<? extends SugarHolder>, Integer> getLayoutResMap() {
return mLayoutResMap;
}

@NonNull
public Map<Class<? extends SugarHolder>, Class> getDataClassMap() {
return mDataClassMap;
}

@Override
@LayoutRes
public int getLayoutRes(@NonNull Class<? extends SugarHolder> holderClass) {
return mLayoutResMap.get(holderClass);
}

@Override
@NonNull
public Class getDataClass(@NonNull Class<? extends SugarHolder> holderClass) {
return mDataClassMap.get(holderClass);
}
}

这个文件通过 HashMap 建立了两个映射关系

  • ViewHolder 和其对应的 xml 布局文件的 id
  • ViewHolder 和其对应的 Model 类

BarHolder$InjectDelegateImpl.java

在 build/generated/source/apt/debug/ 目录下

1
2
3
4
5
6
7
8
9
10
public final class BarHolder$InjectDelegateImpl implements InjectDelegate {
@Override
@SuppressLint("ResourceType")
public <SH extends SugarHolder> void injectView(@NonNull SH sh, @NonNull View view) {
if (sh instanceof com.zhihu.android.sugaradapterdemo.holder.BarHolder) {
com.zhihu.android.sugaradapterdemo.holder.BarHolder th = (com.zhihu.android.sugaradapterdemo.holder.BarHolder) sh;
th.mTextView = (androidx.appcompat.widget.AppCompatTextView) view.findViewById(com.zhihu.android.sugaradapterdemo.R.id.text);
}
}
}

这个文件,也是做了两件事

  • ViewHolder 的强制类型转换
  • ViewHolder 对应控件的 findViewById 操作

下面就从 SugarAdapter 最基础的用法看看他的具体实现

SugarAdapter

1
2
3
4
5
6
7
8
9
10
11
12
13
List<Object> list = new ArrayList<>();
SugarAdapter adapter = SugarAdapter.Builder.with(list)
.add(FooHolder.class)
.add(BarHolder.class)
.build();


recyclerView.setAdapter(adapter);
for (int i = 0; i < 100; i++) {
String text = String.valueOf(i);
list.add(i % 2 == 0 ? new Foo(text) : new Bar(text));
}
adapter.notifyDataSetChanged();

以上就是 SugarAdapter 最基础的用法,可以看到创建 adapter 的操作非常简单。这里添加了两个 ViewHolder,数据类型也有两种,实际显示的 RecyclerView 会根据数据类型,分别显示这两种卡片。那么他是怎么做到呢,下面就从他的实现开始看看。

Build 模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public final class SugarAdapter extends RecyclerView.Adapter<SugarHolder> {
private static final String TAG = "SugarAdapter";

public static final class Builder {
private List<?> mList;
private SparseArray<Container> mArray;

@NonNull
public static Builder with(@NonNull List<?> list) {
return new Builder(list);
}

private Builder(@NonNull List<?> list) {
mList = list;
mArray = new SparseArray<>();
}

@NonNull
public <SH extends SugarHolder> Builder add(@NonNull Class<SH> holderClass) {
return add(holderClass, null);
}

....

@NonNull
public SugarAdapter build() {
if (mArray.size() <= 0) {
throw new IllegalStateException("must add at least one Class<? extends SugarHolder>");
}

return new SugarAdapter(mList, mArray);
}
}
}

本质上来说,SugarAdapter 是继承自 RecyclerView.Adapter,泛型里的 ViewHolder 用的是 SugarHolder。首先这里通过一个 Builder 模式构建了所有需的数据的集合。

Container
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Container {
private Class<? extends SugarHolder> mHolderClass;
private Class<?> mDataClass;
private int mLayoutRes;
private SugarHolder.OnCreatedCallback mCallback;
private Object mData;

Container(@NonNull Class<? extends SugarHolder> holderClass,
@NonNull Class<?> dataClass, @LayoutRes int layoutRes,
@Nullable SugarHolder.OnCreatedCallback callback) {
mHolderClass = holderClass;
mDataClass = dataClass;
mLayoutRes = layoutRes;
mCallback = callback;
}

.....
}

Container 是对所有需要用到的对象类型的包装.

add() 的具体实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@NonNull
public <SH extends SugarHolder> Builder add(
@NonNull Class<SH> holderClass, @Nullable SugarHolder.OnCreatedCallback<SH> callback) {
ContainerDelegate delegate = Sugar.INSTANCE.getContainerDelegate();
Class dataClass = delegate.getDataClass(holderClass);
int layoutRes = delegate.getLayoutRes(holderClass);

if (layoutRes == 0) {
throw new IllegalStateException(holderClass.getCanonicalName()
+ " must have an annotation @Layout(R.layout.*)");
}

mArray.put(holderClass.hashCode(), new Container(holderClass, dataClass, layoutRes, callback));
return this;
}

add 的实现很简单,就是通过 Sugar 这个单例类通过反射的方式获取到 ContainerDelegateImpl 的实例,前面已经说过了,这个实现其实就是一个 ViewHolder 和其对应的 数据类型及布局文件的一个映射集合。因此,这一步要做的就是通过 holder 从映射集合中获取到当前 ViewHolder 对应的数据类型和布局文件 xml 文件的 id

关键的步骤

1
2
Class dataClass = delegate.getDataClass(holderClass);
int layoutRes = delegate.getLayoutRes(holderClass);

Sugar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
enum Sugar {
INSTANCE;

private ContainerDelegate mContainerDelegate;
private Map<Class<? extends SugarHolder>, InjectDelegate> mInjectMap;

@NonNull
ContainerDelegate getContainerDelegate() {
if (mContainerDelegate == null) {
try {
Class delegateClass = Class.forName("com.zhihu.android.sugaradapter.ContainerDelegateImpl");
mContainerDelegate = (ContainerDelegate) delegateClass.newInstance();
} catch (Exception e) {
throw new RuntimeException(e);
}
}

return mContainerDelegate;
}
}

至此,在 build 方法调用的瞬间,就如何解决了之前的痛点做好了铺垫。ViewHolder,Model.class,R.layout.xxx 之间的对应关系已经被梳理好,并保存在 mArray 这个集合中了。

通过上图可以清晰看到各个之间的关系了,Sugar 可以理解为 SugarAdapter 和 SugarHolder 的代理类,方便其获取生成类的实例。最终在 SugarAdapter 的 mArray 集合中存储着所有关键信息,下面就看看这些关键信息是如何被使用的。

痛点如何被消除

按照之前的常规的逻辑,痛点无非就是新增样式(ViewHolder)需要 getItemViewType,onCreateViewHolder,onBindViewHolder 这三个方法轮流改一遍。那么有了之前的铺垫,SugarAdapter 又是如解决这些痛点的呢?
我们还是回到 SugarAdapter 中。

getItemViewType

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public int getItemViewType(@IntRange(from = 0) int position) {
Object data = mList.get(position);

....

for (int i = 0; i < mArray.size(); i++) {
int key = mArray.keyAt(i);
Container container = mArray.get(key);
if (container.getDataClass() == data.getClass()) {
container.setData(data);
return key;
}
}

throw new RuntimeException("getItemViewType failed, data: " + data.getClass().getCanonicalName()
+ ", please make sure you have associated it with a Class<? extends SugarHolder>");
}

没错,就是这么简单。第一步,获取数据集合中,当前位置的数据类型,然后遍历之前保存好的 mArray 集合,找的那个与当前数据类型匹配的 key (也就是 viewtype) 返回即可。本质来说,确定 viewType 类型的方式还是没变,即数据决定类型,但是由于之前的一系列铺垫准备,现在这里就可以通过数据直接自动查找了。

回到之前 add 方法 的实现,可以看到,在进行 put 操作时,使用的 key 是 viewholder 实例的 hashcode(),因此 这里返回的其实就是 viewholder 的 hashcode,因此,可以理解为 SugarAdapter 是用 ViewHolder hashcode 作为 viewtype 的取值。

onCreateViewHolder

viewtype 确定了,那么就可以创建这个 type 对应的 ViewHolder 了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public SugarHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
Container container = mContainerArray.get(viewType);

try {
View view = null;
int layoutRes = container.getLayoutRes();

... preInflate view stuff ...

if (view == null) {
view = inflateView(layoutRes, parent);
}

SugarHolder holder = container.getHolderClass().getDeclaredConstructor(View.class).newInstance(view);
holder.setAdapter(this);
holder.setData(container.getData()); // makes SugarHolder#getData non-null

... lifecycle and callback stuff ..

return holder;
} catch (@NonNull Exception e) {
Log.e(TAG, "onCreateViewHolder failed, holder: " + container.getHolderClass().getCanonicalName());
throw new RuntimeException(e);
}
}

这里的 mContainerArray 其实就是之前一直在说的 mArray。是以 viewholder 的 hashcode (viewtype) 为 key ,Container(包含这个 viewholder 数据类型,自身,R.layout.xxx 等内容的包装类) 为值的一个 SparseArray。

因此,到了这里就很简单了,根据 viewtype 从 Container 的 map 集合中取出对应的 Container ,使用 R.layout.xxx inflate view, 然后创建 viewholder 的实例返回。当然为了性能提升,inflate view 之前,还会有 preInflate 的逻辑,具体细节可以参考源码,这里不做重点讨论。

onBindViewHolder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
private void onBindViewHolderInternal(@NonNull SugarHolder holder, int position, @Nullable List<Object> payloads) {
Object data = mList.get(position);
holder.setData(data); // double check

if (payloads == null || payloads.isEmpty()) {
holder.onBindData(data, Collections.emptyList());
} else {
holder.onBindData(data, payloads);
}

holder.getLifecycleRegistry().handleLifecycleEvent(Lifecycle.Event.ON_START);

for (SugarHolderListener listener : mSugarHolderListenerList) {
if (listener.isInstance(holder)) {
listener.onSugarHolderBindData(holder);
}
}
}

到这里就很简单了,获取 position 对应的数据,调用 holder.onBindData 方法即可,还记得这个方法吗,就是在继承 SugarHolder 时需要实现 UI 和数据绑定的那个方法。

可以看到,SugarAdapter 还帮我们添加了 lifecycle 相关的回调,包括在 onCreateViewHolder 时也会有 。因此使用 SugarAdapter 还可以方便的感知到更多的生命周期回调,获取到更多的方法执行结点。

小结

至此,我们了解到 SugarAdapter 解决痛点最关键,最核心的逻辑,就是在 add(SugarHolder holder) 的时候,根据编译期生成的文件,建立好了最终需要的对应关系集合(包含 Container 的 mArray),从而在后续方法执行的时候,可以根据已有的对应关系,非常方便的自动实现 viewtype–>view–>viewholder–>Data–>UI 最终对应关系。

SugarAdapter 还能做什么

“一种” 数据对应不同的样式

通过之前的分析,我们可以看到使用 SugarAdapter ,在其 getItemViewType 方法,会根据当前 position 从数据列表中获取当前位置的 Model,然后由这个 model 决定 viewType ,也就是说数据决定了在 RecyclerView 中当前这个 position 用哪种 ViewHolder。

然而,有时候 ViewHolder (即卡片样式) 并不一定由完全数据类型决定,而是由 Model 中的某个字段的类型决定,甚至面临着相同的数据,在不同的位置需要不同的 ViewHolder,甚至极端情况下,相同位置需要使用的 ViewHolder 是需要动态变化的。

比如图中常见的 feed 流式新闻,卡片 1 和 3 基本上是相同的,卡片 2 却是完全一种不同的样式。一般情况下,服务端下发的数据一定是同类型的,顶多加几个字段,区分一下这些内容。如上所说,SugarAdapter 是根据数据类型决定 ViewHolder,那么数据类型如果相同,那么该如何处理呢?这就要靠 Dispatcher 了。这里看一下官方的例子就明白了。

1
2
3
4
5
6
7
8
adapter.addDispatcher(Foo.class,new SugarAdapter.Dispatcher<Foo>() {
@SuppressWarnings("ConstantConditions")
@Override
@Nullable
public Class<? extends SugarHolder> dispatch(@NonNull Foo foo) {
return Integer.parseInt(foo.getText()) % 2 == 0 ? FooHolder.class : FooHolder2.class;
}
});

同样的数据类型 Foo,但我们可以根据这个数据的细节,自己决定使用哪种类型的 ViewHolder (但前提是需要把用到 ViewHolder add 进去,否则是找不到的)

规则

那么这个 Dispatcher 是如何的原理又是什么呢?

1
2
3
4
5
public static abstract class Dispatcher<T> {

@Nullable
public abstract Class<? extends SugarHolder> dispatch(@NonNull T data);
}
  • 首先可以看到,他就是一个抽象类。SugarAdapter 内部维护着一个 Dispatcher 的集合。
1
private Map<Class<?>, Dispatcher<?>> mDispatcherMap;

当我们进行 addDispatcher 的操作时,就会把一个实际的 Dispatcher 实现添加到这个集合中。而在 onCreateViewHolder 方法执行时,会优先从这个 map 里获取对应的 ViewHolder,然后通过 viewholder 反向确定 viewtype 。这里这样做虽然有点绕,但是这样 dispatcher 就可以屏蔽数据决定 viewtype 的流程,从而巧妙地解决了同类型数据,需要不同样式的难题。

如果找到了就直接返回 viewtype; 如果没找到就会从 mArray (所有对应关系的集合中)查找,不影响其余的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public int getItemViewType(@IntRange(from = 0) int position) {
Object data = mList.get(position);

Class<? extends SugarHolder> holderClass = null;
if (mDispatcherMap.containsKey(data.getClass())) {
Dispatcher dispatcher = mDispatcherMap.get(data.getClass());
holderClass = dispatcher.dispatch(data);
}

if (holderClass != null) {
int key = holderClass.hashCode();
if (mContainerArray.indexOfKey(key) < 0) {
throw new RuntimeException("getItemViewType() failed, holder: " + holderClass.getCanonicalName()
+ ", please make sure you have added it when build SugarAdapter.");
}

mContainerArray.get(key).setData(data);
return key;
}

... search key from mArray stuff...
}

这就是 Dispatcher ,Dispatcher 就是用来动态分发 ViewHolder 的,具体如何动态分发,就看实际的场景了。

生命周期回调 和 Lifecycleowner 的支持。

SugarAdapter 内部有很多回调,可以监听 SugarAdapter 执行的各个生命周期,这些生命周期和 RecyclerView 自身也是相关的。甚至如果你要使用 lifecycle-aware(生命周期可感知的)组件,也很非常方便的。更多细节可以参考 具体实现

总结

总的来说,对于包含多种样式的 RecyclerView 来说,SugarAdapter 是比较合适的。更重要的是,SugarAdapter 为我们解决问题,提供了一种思路。对于一些需要重复执行,但又很有规律的代码逻辑;找到问题的本质,从最痛的地方出发,往往就能解决问题。

加个鸡腿呗.