MultiType 是一个为 RecycleView 创建多种 Item 的 Android 库,它的设计简洁优雅,源码阅读体验也很好,本文记录了笔者研读此项目源码的感悟。
MultiType 的简单使用
开始之前,先来看下 MultiType 的总体类图
图中左侧 MultiTypeAdapter
是供外部访问的主要类,它的使用方式如下。
|
|
图中ItemViewBinder
是一个负责创建 ItemView 与填充 Item 数据的抽象类,上述代码中的 TextItemViewBinder
、ImageItemViewBinder
与 RichItemViewBinder
均为ItemViewBinder
的实现类。
ItemViewBinder
的代码看起来很直观,以ImageItemViewBinder
为例,它的实现如下。
|
|
可以看到,MultiType 将 Item 视图创建与数据填充的工作交给了 ItemViewBinder
来完成。当有新类型的 ItemView 需求时,只需先创建一个继承于 ItemViewBinder
的具体类,再将其注册到 MultiTypeAdapter
中即可。
MultiType 源码解析
1. MultiTypeAdaprer 的 register 过程
从 MultiTypeAdapter
入手,它有如下三个 成员变量。
|
|
以文章开头处的代码为例,先来看一下 MultiTypeAdapter
的成员函数 register 的执行流程
title: MultiTypeAdapter register 工作流程
Client->MultiTypeAdapter: 1. register(ItemViewBinder<T,*>)
MultiTypeAdapter->MultiTypeAdapter: 2. register(Class<T>,ItemViewBinder<T,*>)
MultiTypeAdapter->MultiTypeAdapter: 3. register(Type<T>)
MultiTypeAdapter->MutableTypes: 4. register(Type<T>)
1.1 register(binder: ItemViewBinder<T, *>)
|
|
函数中将 T
声明为 refied ,表示 T
是一个具体的类型,随后在函数内部调用了同名重载函数 register(clazz: Class<T>, binder: ItemViewBinder<T, *>)
1.2 register(clazz: Class, binder: ItemViewBinder<T, *>)
|
|
若已使用 clazz 注册过,则先对其进行清除。接着将前面传递来的参数 clazz 、binder 与新建的DefaultLinker 对象 一起作为 Type
构造函数的参数,构建 Type
对象。然后调用同名重载函数register(type:Type<T>)
。
Type
是一个只有三个属性的 data class,它的实现如下
|
|
1.3 register(type: Type)
|
|
此处将上一步传来的 type对象 注册到 MutableTypes
类型的成员变量 types 中,同时将 MultiTypeAdapter
自身的引用赋值给 type对象 中成员变量 binder 的属性 _adapter ,便于后续的使用。
接下来看 MutableTypes
中 register 函数的实现
1.4 MutableTypes.register(type:Type)
|
|
MutabaleTypes
有属性 initialCapacity 、 types ,前者表示后者的默认容器大小,types 则是一个可变列表。另外还可以看到,MutbaleTypes 实现了 Types 接口,并重写了register 函数,此函数是将传入的参数 type 添加到 types 列表中。与 register 函数相反的还有另一个重写函数 unregister ,它是将某个指定的 Class
所对应的全部 Type 对象从 types 列表中移除。
1.5小结
结合以上四步操作,可知 MultiTypeAdapter
的 register 函数实际是将待注册的 数据模型 Class 信息、数据模型对应的 ItemViewBinder、数据模型对应的 Linker(默认为 DefaultLinker) 组合为 Type
对象,然后将其添加到 MultiTypeAdapter
的属性 types 对应的MutableTypes
对象的属性 types 可变数组中,以供后续使用。
2. MultiTypeAdapter 中 ViewHolder 创建与数据填充的过程
MultiTypeAdapter
是 RecyclerView.Adapter
的实现类,RecyclerView.Adapter
中与 ViewHolder 创建及数据填充的相关的函数主要有以下四个:
- getItemViewType(position: Int): Int // 获取每一个 Item 对应的 View 类型
- onCreateViewHolder(parent: ViewGroup, indexViewType: Int): ViewHolder // 为特定类型的 Item 创建 ViewHolder
- onBindViewHolder(holder: ViewHolder, position: Int) //为每一个 Item 进行数据填充
- getItemCount(): Int // 获取 Adapter 内 Item 的数量
我们主要观察 MultiTypeAdapter
中前三个函数的实现
2.1 getItemViewType(position: Int): Int
MultiTypeAdapter->MultiTypeAdapter: 1.indexInTypesOf
MultiTypeAdapter->MutableTypes: 2.firstIndexOf
MutableTypes-->MultiTypeAdapter: index
MultiTypeAdapter->MutableTypes: 3.getType
MutableTypes->MutableTypes: getLinker
MutableTypes-->MultiTypeAdapter: linker
|
|
先来看 getItemViewType 函数, 第8行代码,调用了 indexInTypesOf 函数,传递给后者的参数为当前 item 在列表中的位置 position 以及这个 item 对象。
再来看 indexInTypesOf 函数,第13行代码,成员变量 types 的 firstIndexOf 函数通过 item 的 Class
信息查询这个 item 在 types 中首次出现的索引值,若得到的索引值不是 -1 ,则表示表示与此 item 的 Class
信息相关联的 Type
对象已注册到 MutbleTypes
中;若得到的索引值是 -1 ,则表示与此 item 的 Class
想关联的 Type
对象未注册,并在第18行抛出自定义的运行时异常BinderNotFoundException
。types 是类型为 MutableTypes
的对象,MutableTypes
的 firstIndexOf 函数的实现如下。
|
|
MutbaleTypes 的 firstIndexOf 函数的形参是 Class
类型的 clazz
对象,第7行代码,在成员变量 types 列表中查找与 clazz 属性与形参一致的首个 Type
对象的索引值。若能查到(不为 -1),则直接返回此索引;若未查到,则尝试使用入参 clazz 的继承关系重新在成员变量 types 内查询一次,其中 A.isAssignableFrom(B)
表示判断A
的 Class
信息是否为 B
的 Class
信息的接口或父类。
回到 indexInTypesOf 函数,第16行代码,返回上一步得到的索引值 index 与 linker.index(posotion,item) 相加的值,linker 的默认实现为 DefaultLinker
,其 index 函数的返回值始终为 0 ,因此此处相加的值即为上一步得到的索引值 index,对于 linker 的特殊实现我们稍后在第三小节单独分析,此处暂且不表。
至此,我们知道 getItemViewType 函数的返回值 viewType 即为当前 item 的 Class
信息在 MutableTypes
的成员变量 types 列表中索引值。
2.2 onCreateViewHolder(parent: ViewGroup, indexViewType: Int): ViewHolder
MultiTypeAdapter->LayoutInflater: 1. from
LayoutInflater-->MultiTypeAdapter: inflater
MultiTypeAdapter->MutableTypes: 2. getType
MutableTypes->MutableTypes: 3. getBinder
MutableTypes-->MultiTypeAdapter: binder
MultiTypeAdapter->ItemViewBinder:4. onCreateViewBinder
|
|
当 RecyclerView 需要一个新的 ViewHolder 时,onCreateViewHolder 函数会被调用,传入的 parent 表示RecycleView ,indexViewType 表示这个 item 对应的视图类型,即在 getItemViewType
函数的返回值
第8行代码从传入的 ViewGroup
类型的 parent 参数中获取 context 对象,然后将此 context 作为参数调用 LayoutInflater
的静态方法 from ,得到 Inflater
类型 inflater 的对象。
第9行代码,通过传入的 indexViewType 参数,先调用属性 types 的 getType 函数获取 Type
类型的对象,然后得到 Type
对象的属性 binder 。 getType 函数的实现如下所示,只是将 index 对应的 Type
从 types 列表中取出,这个 Type
对象是在初始化时通过 register 函数添加到 types 列表中的,我们在第一小节中有介绍。
|
|
第10行代码,调用上一步得到的 binder 对象的成员函数 onCreateViewHolder 来完成 ViewHolder 的创建。
至此,我们了解了 ViewHolder 的创建过程。
2.3 onBindViewHolder(holder: ViewHolder, position: Int)
MultiTypeAdapter->MultiTypeAdapter: 1. onBindViewHolder
MultiTypeAdapter->MultiTypeAdapter: 2. onBindViewHolder
MultiTypeAdapter->MultiTypeAdapter: 3. getOutBinderByViewHolder
MultiTypeAdapter->MutableTypes: 4. getType
MutableTypes-->MultiTypeAdapter: type
MultiTypeAdapter->Type: 5.getBinder
Type-->MultiTypeAdapter : binder
MultiTypeAdapter->ItemViewBinder: 6. onBindViewHolder
|
|
当 RecyclerView 需要显示指定 position 位置的 Item 时,MultiTypeAdapter
的 onBindViewHolder 函数会被调用。
第7-9行代码,函数的形参 holder 是在 onCreateViewHolder 函数中创建的,position 是当前 Item 对应的位置。第8行代码调用了同名重载函数。
第11-14行代码,函数的形参中多了一个用于优化数据填充的 payloads 参数。第12行,先获取 position 对应的 item 对象。第13行,先通过成员函数 getOutBinderByViewHolder 获取具体的 ItemViewBinder
对象。
getOutBinderByViewHolder 函数的实现也很直观。第19行代码,先通过MultiTypes
类型的属性 types 的成员函数 getType 得到函数形参传递而来的 holder 对象的 itemViewType 属性所对应的 Type
对象,然后将此Type
对象的 binder 属性强转为 ItemVIewBinder
返回给调用方。
回到 onCreateViewHolder 函数,第13行代码,在得到当前 holder 对应的ItemViewBinder
之后,接着便调用了这个ItemViewBinder
对象的成员函数 onBindViewHolder,伴随的参数的是 holder,item与上一步传来的 payloads,从而完成了这个 ViewHolder 的数据填充。
2.4 小结
第一小节中,MultiTypeAdapter
的 register 函数先将数据模型的 Class 信息、具体的 ItemViewBinder 以及 linker 封装为 Type
对象并添加到 MutbaleTypes
的成员变量 types 列表中。
本小节中解析的三个函数,均与第一小节中介绍的 MutableTypes
有关。
- getItemViewType 函数根据当前 item 的 Class 信息获取其所对应的 Type 对象在 types 列表中的索引值;
- onCreateViewHolder 函数根据 getItemViewType 计算出的索引值在 types 列表中获取具体 ItemViewBinder, 然后使用调用这个 ItemViewBinder 的 onCreateViewHolder 函数创建具体的 ViewHolder;
- onBindViewHolder 函数根据 onCreateViewHolder 中创建的 holder 对象的成员变量 itemViewType 获取这个 holder 对应的 Type 对象的成员变量 binder,然后调用 binder 的 onBindViewHolder 完成数据填充。
3. MultiType 一对多
MultiType 中的一对多是指 RecyclerView 中的同一份数据模型,对应多种 ViewHolder。如下所示,电商 App 的搜索结果页就属这种情况。
列表模式 | 大图模式 |
---|---|
3.1 one2many 简单实用
下边以 MultiType 中的sample one2many 为入口,来了解一下 MultiType 是如何实现一对多的。开始之前,先来看一下 one2many 的运行截图。
one2many sample 中包含以下四个类,其中 Data
是数据模型类,它有 DataType1ViewBinder
和 DataType2ViewBinder
两种样式。OneDataToManyActivity
是这个 sample 的 Activity。
one2many
Data.kt
DataType1ViewBinder.kt
DataType2ViewBinder.kt
OneDataToManyActivity.kt
Data
类的代码如下,它有 title 和 type 两个成员变量,前者用来显示,后者表示其对应的 ViewBinder
类型。
|
|
DataType1ViewBinder
和 DataType2ViewBinder
的区别仅是 layout 不同,如下所示。
|
|
R.layout.item_data_type1 与 R.layout.item_data_type2 的区别仅是 gravity 不同,如下所示。
|
|
OneDataToManyActivity
中的代码如下,其 register 部分与第一小节有明显不同。
|
|
3.2 one2many 的 register 过程
Client->MultiTypeAdapter: 1. register
MultiTypeAdapter->MultiTypeAdapter: 2. register
MultiTypeAdapter-->Client: oneToManyBuilder
Client->OneToManyBuilder: 3. to
Client->OneToManyBuilder: 4. withKotlinClassLinker
OneToManyBuilder->OneToManyEndpoint: 5. withKotlinClassLinker
OneToManyEndpoint->OneToManyEndpoint: 6. withJavaClassLinker
OneToManyEndpoint->OneToManyBuilder: 7. withJavaClassLinker
OneToManyBuilder->ClassLinkerBridge: 8.toLinker
ClassLinkerBridge-->OneToManyBuilder: linkerBridge
OneToManyBuilder->OneToManyBuilder: 9. withLinker
OneToManyBuilder->OneToManyBuilder: 10. doRegister
note left of OneToManyBuilder: loop: binders
OneToManyBuilder->MultiTypeAdapter: 11. register
MultiTypeAdapter
一对多的 register 函数实现如下。
|
|
register 函数,第9行代码,获取调用方传来的 KClass
对象的 javaClass 变量,然后调用同名重载函数。第14行代码,尝试移除此 javaClass 已经关联的 Type
。第15行代码,创建 OneToManyBuilder
对象并返回给调用方。
|
|
回到 OneDataToManyActivity 的 onCreate 方法。第1行代码,在调用 MultiTypeAdapter
的 register 函数后得到了 OneToManyBulder
对象,紧接着调用该对象的 to 函数,并将 DataType1ViewBinder()
, DataType2ViewBinder()
作为参数传入。第30-37行代码,调用 OneToManyBulder
对象的 withKotlinClassLinker 函数,并新建了一个 KotlinClassLinker
的匿名内部类的对象作为此函数的实际参数。
接下来我们来看下 OneToManyBulder
的代码,它实现了 OneToManyFlow
和 OneToManyEndpoint
这两个接口。
|
|
OneToManyBuilder 有 adapter,clazz 两个属性,如之前所述,这两个属性在 MultiTypeAdapter
的 register 函数中被赋值。
第10行代码,to(vararg binders: ItemViewBinder<T, *>)
函数声明中的的 vararg
关键字,对应 Java 中的可变参数符号 …
,此处表示 to 函数可以接受一个或多个 ItemViewBinder
类型的对象作为参数。to 函数的实现是将调用方传入的参数 binders 赋值给 OneToManyBuilder
的属性 binders。
OneToManyBuilder
并未重写 withKotlinClassLinker 函数,因此当调用方调用此函数时,会调用到OneToManyEndpoint
接口内的默认函数 withKotlinClassLinker。此函数有两个重载版本,一个接受 KotlinClassLinker
类型的对象作为参数,另一个接受(position: Int, item: T) -> KClass<out ItemViewBinder<T, *>>)
类型的 lambda 变量作为参数,这两个默认函数内部都会调用第48行的默认函数 withJavaClassLinker 来将上层调用方传来的 KotlinClassLinker
类型的对象转换为 JavaClassLinker
类型的对象。
默认函数 withJavaClassLinker 内部又调用了同名重载函数 withJavaClassLinker ,并将新建的JavaClassLinker
对象作为参数传递过去。
OneToManyBuilder
重写了这个重载的 withJavaClassLinker 函数,第19行代码,先将上一步传来的 javaClassLinker 与它的 binders 属性作为参数,调用了 ClassLinkerBridge
的静态函数 toLinker 将其转化为ClassLinkerBridge
类型的对象,ClassLinkderBridge
的实现如下。
|
|
ClassLinkerBridge
实现了 Linker
接口并重写了此接口的 index 函数。第7行代码,通过调用方传入的 javaClssLinker 参数,获取 position 与 item 所对应的 ItemViewBinder
的 Class
信息。第8-12 行代码,获取这个 Class
对应的 ItemViewBinder
在属性 binders 列表中的索引值,若查找不到,则抛出自定义异常 IndexOutOfBoundsException
。
回到OneToManyBuilder
中,第19行代码,将上一步得到 ClassLinkerBridge
对象作为参数调用了第15行的withLinker 函数,此函数内部调用了第23行的 doRegister 函数。doRegister 函数内对OneToManyBuilder
的属性 binders 进行遍历,并将其中的每一个 binder与 clazz 属性和传入的参数linker组合为 Type
对象,注册到 MutableTypes
中。因此对于同一份数据模型,会在 MutableTypes
中注册多个 Type
。此处索引值的设计思路非常巧妙,随后会单独讨论。
3.3 one2many 中 ViewHolder 的创建
Client->MultiTypeAdpter: 1. getItemViewType
MultiTypeAdpter->MultiTypeAdpter: 2. indexInTypesOf
MultiTypeAdpter->MutableTypes: 3. firstIndexOf
MutableTypes-->MultiTypeAdpter: firstIndex
MultiTypeAdpter->MutableTypes: 4. getType
MutableTypes->MutableTypes: getLinker
MutableTypes-->MultiTypeAdpter: linker
MultiTypeAdpter->ClassLinkerBridge: 5. index
ClassLinkerBridge-->MultiTypeAdpter: linker.index
MultiTypeAdpter-->Client: firstIndex + linker.index
通过前面的分析我们知道,RecyclerView
中 ViewHolder 的创建与 ItemViewType 有关,而 ItemViewType 源于 Adapter 中的 getItemViewType 函数,我们再来看一下MultiTypeAdapter
中 getItemViewType 的实现。
|
|
第8行代码,根据当前索引得到在 MutableTypes
中注册的 Type
对象的属性 linker ,这个 linker 便是我们在 3.2中见到过的ClassLinkerBridge。第9行代码,调用这个 linker 对象的 index 函数,index 的实现如下。
|
|
第6行代码,index 函数调用了 JavaClassLinker
对象的 index 函数。JavaClassLinker
对象是在注册过程中经 withJavaClassLinker 函数转化而来的,此函数将调用方传入的 KotlinClassLinker
类型的对象转换为JavaClassLinker
类型的对象,我们在 3.2 中已做过分析,此处不再展开。转换前的 kotlinClassLinker 如下所示,根据传入的参数 data 的成员变量 type 来决定返回的ItemViewBinder
的类型。
|
|
one2many sample 中这样处理是有意演示 KotlinClass 的使用方式,平时用的话可以直接条用 withJavaClassLinker 来省略这一步的转换。
回到 ClassLinkerBridge
的 index 函数,第6行代码得到了当前 Item 所对应的具体的Class<ItemViewBinder>
对象。第7-11行代码,得到该 class 对象在属性 binders 数组中的首次出现的索引值,然后将其返回调用方。
再回头看 MultiTypeAdapter
中 indexInTypesOf函数的实现。第9行代码,将这个 item 在 Types 中首次出现的索引值与它在 linker 的 binders 列表中的索引值相加,作为getItemViewType
函数的返回值。
接下来是我们之前分析过的 onCreateViewHolder
,这次我们主要看第3行代码,根据 getItemViewType 获取的 indexViewType ,得到其对应的 ItemViewBinder。
|
|
3.3 小结
之前说到 MultiType 中索引值的设计非常巧妙,在了解了一对多的注册、使用后,再来探索下这个巧妙之处。 one2many 的 sample 的注册过程中,在 MutableTypes
中注册了多个 Type
对象,注册后 **types:List<Type>**的数据如下
index | Class | ItemViewBinder | Linker |
---|---|---|---|
0 | Data | DataType1ViewBinder | ClassLinkerBridge |
1 | Data | DataType2ViewBinder | ClassLinkerBridge |
getItemViewType 中返回的索引值是当前位置的 Item 对应的 Type
在MutableTypes
的属性 types 列表中首次出现的索引值 index 与 这个索引值所对应的 Type
对象的 linker 属性的 index 函数的返回值 indexFromLinker 的和。
例1:
Data
对应的Type
在 types中首次出现的索引值为 0,当Data
对象的type属性为 TYPE_1 时,indexFromLinker 的值为 0,此时 getItemViewType 的返回值是 0。
onCreateViewHolder 函数的参数 indexViewType 即为上一步得到 index 与 indexFromLinker 和,此函数内调用 MutbaleTypes
类型属性 types 的 getType 函数,将上一步得到的和作为 getType 的参数传递过去得到 Type
对象,然后通过得到了这个Type
对象的 binder属性。这个 binder 刚好和上一步中 ClassLinkerBridge
的 index 函数中 javaClassLinker.index 所对应的 ItemViewBinder
一致。
例2:传入的 indexViewType 值为 0,因此 **types.getType(indexViewType)**得到的
Type
的即为表格中第一行的数据,它的 binder 属性即为 DataType1ViewBinder ,与例1中Data
对象的 type 属性相匹配。
技术点拾遗
MultiTypeAdapter 中重写了
fun onBindViewHolder(VH holder, int position, List<Object> payloads)
函数,此函数对 ViewHolder 的 部分数据更新 进行了优化。具体表现是当 payloads 列表不为空时,仅更新 payloads 内的部分数据,减轻渲染压力达到优化效果。若要在 kotlin 泛型函数中使用泛型
T
的 Class 信息,需在T
之前增加reified
关键字,同时在函数前增加inline
关键字,如下所示
|
|
- kotlin 的构造函数自带的重载效果仅限 kotlin 模块可用,若要在 Java 模块中使用 kotlin 构造函数的重载函数,可在构造函数前添加
@JvmOverloads
注解,如下所示。
|
|
- kotlin 中的泛型没有表示不确定类型的符号
?
,但有称为 Star-projections 的符号*
,直译过来为星形投影。它是更为安全及严格的泛型限定符,编译器会将<*>
视为<out Any>
,其中的out
表示该类型只能作为函数的返回值类型使用,若将其作为函数的入参使用,编译器会直接报错。具体规则如下:
Function<*>
表示Function<in Nothing>
和Function<out Any?>
Function<*, String>
表示Function<in Nothing, String>
;Function<Int, *>
表示Function<Int, out Any?>
;Function<*, *>
表示Function<in Nothing, out Any?>
.