DslAdapter 开发简介
DslAdapter 是一个 Android RecyclerView 的 Adapter 构建器, DSL 语法, 面向组合子设计. 专注类型安全, 所有代码采用 Kotlin 编写.
为什么要开发 DslAdapter
实际上在 DslAdapter 开始开发的时点已经有很多 RecyclerAdapter 的扩展库存在了, 有的甚至从 2015 年开始已经持续开发到了现在. 从功能上来说, 这些库的各项功能都非常成熟了, 几乎所有的需求都有涉及; 而从思想上来说, 各种构建方式都有相应的库
以现在很常见的库举例:
CymChad/BaseRecyclerViewAdapterHelper 1w6 star 这是 Github 上检索出 Star 最多的 RecyclerAdapter 的库, 它支持添加 Item 事件、添加列表加载动画、添加头部、尾部、树形列表等等,甚至设置空布局
FastAdapter 这个库是以前项目中也使用过的库, 功能也相当丰富, 相比上面的库更注重 List 的功能, 是一个从 2015 年开始开发一直持续维护的库
KidAdapter kotlin 编写,DSL 语法的构建器,利用了 kotlin 的语法特性,构建上更简单,同时也实现了 types 功能。但功能上相比上面的库就简单很多了
甚至我多年前也已经写过一个相关库 AdapterRenderer,也实现了很多功能。可以说 RecyclerAdapter 领域是最难以有新突破的地方了,似乎能做的只是在现有基础上小修小改而已了。
但现有的库真的已经完美到这种程度了吗?
不,现有的库也有很多的缺点:
非强类型,为了兼容多类型而直接忽略数据类型信息,失去了编译期类型检查,只能靠编写时自我约束
繁琐的模板代码。有些库会需要继承基础 Adapter 或者基础 Holder 继承方法来实现功能,因此会写大量的 XXAdapter、XXItem
逻辑代码被迫分离。也是由于需要单独写 XXAdapter、XXItem,而这些小类抽象度低,该界面的逻辑被迫分离到了这些小类中,即使是业务联系很大的逻辑
功能过于繁杂,抽象度低。功能上虽然丰富,但实际上包括了很多并不是 RecyclerView 应该关注的功能,导致变成了一个全功能的 Adapter,Adapter 的逻辑结构极其复杂,难以维护也难以扩展
类中变量和算法混杂,需要时常注意状态的同步问题
正因如此,为了解决以上这些问题,让我们编写的 Adapter 更简单、更安全,从而有了开发一个新库的想法
一个新的 Adapter 库应该是什么样的
想要构建一个 Adapter 的库,首先我们要想想我们这个新库应该是干什么的,这就需要回到 RecyclerView 这个库中 Adapter 被定义为什么。
RecyclerAdapter 被定义为数据适配器,即将数据和视图进行绑定:
数据 – 映射 –> 视图 List
而 RecyclerView 之所以很强大是因为它已经不仅仅是用于显示 List,它会需要显示复杂的视图结构,比如树状图、可展开列表等等
数据 – 映射 –> 复杂结构 – 渲染 –> 视图 List
我们的 Adapter 库需要完成的工作简单来说就是:将数据变换为复杂的抽象结构(比如树状),再将复杂的抽象结构渲染为视图 List(因为 RecyclerView 最终只支持平整单列表)
开始构建
定义基本组合子
实际我们需要实现的是一个变换问题,无论最终我们需要的抽象结构是简单的 List 还是复杂的树状图本质上都只是做这么一个数据的变换,因此这个变换函数就是我们的基础组合子,我们可以通过基础组合子的相互组合实现复杂功能
这个基本组合子就是 DslAdapter 库中的 BaseRenderer 类:
interface Renderer<Data, VD : ViewData<Data>> {
fun getData(content: Data): VD
fun getItemId(data: VD, index: Int): Long = RecyclerView.NO_ID
fun getItemViewType(data: VD, position: Int): Int
fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder
fun bind(data: VD, index: Int, holder: RecyclerView.ViewHolder)
fun recycle(holder: RecyclerView.ViewHolder)
}
它包含作为 RecyclerAdapter 基础组合子需要的几个基本方法。
数据放在哪儿
函数范式中副作用是要严格分离的,而变量就是一种副作用,如果允许变量在 Renderer 中不受管制的存在会使 Renderer 组合子本身失去可组合性,同时数据也变得很不可靠(线程不安全、Renderer 并不保证只在一个地方使用一次)
而可以看到 Renderer 的基础方法中定义的都是纯函数,并且不包含允许副作用存在的 IO 等类型(这里的 IO 不同于 Java 中的 input/output,而是指 Haskell 中的 IO 类型类),因此数据被设计为与 Renderer 严格分离(数据与算法的严格分离),ViewData 即是这个被分离的数据
interface ViewData<out OriD> : ViewDataOf<OriD> {
val count: Int
val originData: OriD
}
Adapter 设计
根据前一章的描述,我们构建的 Renderer 就是一个映射函数,因此 Adapter 也只需要使用这个映射函数将数据进行渲染即可
class RendererAdapter<T, VD : ViewData<T>>(
val initData: T,
val renderer: BaseRenderer<T, VD>
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
private val dataLock = Any()
private var adapterViewData: VD = renderer.getData(initData)
…
override fun getItemCount(): Int = adapterViewData.count
override fun getItemViewType(position: Int): Int =
renderer.getItemViewType(adapterViewData, position)
override fun getItemId(position: Int): Long =
renderer.getItemId(adapterViewData, position)
override fun onCreateViewHolder(parent: ViewGroup,
viewType: Int): RecyclerView.ViewHolder =
renderer.onCreateViewHolder(parent, viewType)
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) =
renderer.bind(adapterViewData, position, holder)
override fun onFailedToRecycleView(holder: RecyclerView.ViewHolder): Boolean {
renderer.recycle(holder)
return super.onFailedToRecycleView(holder)
}
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
renderer.recycle(holder)
}
}
可以看到 Adapter 实际只是将 Renderer 中的各个函数实际应用于原生 RecyclerView.Adapter 的各个方法中即可,极其简单
另外,ViewData 只保存于 Adapter 中一份,它其实就包含整个 Adapter 的所有状态,换句话说只要保存它,可以完全恢复 RecyclerView 的数据状态;另外 ViewData 是被锁保护起来的,保证数据的线程安全性
定义基础 Renderer 组合子
Renderer 被我们定义为基础组合子,那我们需要哪些 Renderer 呢:
EmptyRenderer: 空 Renderer, count 为 0
LayoutRenderer: 与 View 绑定的末端 Renderer, 可自定义数量
ConstantItemRenderer: 将常量绑定到 View 的末端 Renderer, 可适配任意数据源, 可自定义数量
MapperRenderer: 转换目标 Renderer 的数据源类型, 一般通过 mapT() 来使用它
ListRenderer: 将目标 Renderer 转换为适配列表数据源
SealedItemRenderer: 根据数据源具体数据选择不同的 Renderer 渲染, 比如对于 Int? 类型,可以在为 null 的时候用 EmptyRenderer 渲染; 不为 null 的时候使用 LayoutRenderer 渲染
ComposeRenderer: 组合多个不同 Renderer
DataBindingRenderer : Android Databinding 支持的 Renderer
复杂的结构基本都可以通过组合他们来实现:
val adapter = RendererAdapter.multipleBuild()
.add(layout<Unit>(R.layout.list_header))
.add(none<List<Option<ItemModel>>>(),
optionRenderer(
noneItemRenderer = LayoutRenderer.dataBindingItem<Unit, ItemLayoutBinding>(
count = 5,
layout = R.layout.item_layout,
bindBinding = {ItemLayoutBinding.bind(it) },
binder = {bind, item, _ ->
bind.content = “this is empty item”
},
recycleFun = {it.model = null; it.content = null; it.click = null}),
itemRenderer = LayoutRenderer.dataBindingItem<Option<ItemModel>, ItemLayoutBinding>(
count = 5,
layout = R.layout.item_layout,
bindBinding = {ItemLayoutBinding.bind(it) },
binder = {bind, item, _ ->
bind.content = “this is some item”
},
recycleFun = {it.model = null; it.content = null; it.click = null})
.forList()
))
.build()
以上 Adapter 可图示为:
|–LayoutRenderer header
|
|–SealedItemRenderer
| |–none -> LayoutRenderer placeholder count 5
| |
| |–some -> ListRenderer
| |–DataBindingRenderer 1
| |–DataBindingRenderer 2
| |–…
技术细节
特征类型
上面说到 Renderer 被定义为:Renderer<T, VD : ViewData<T>>, 其中 ViewData 因为和 Renderer 是强绑定的,所以往往一种 ViewData 和一种 Renderer 是一一对应的,在类型上 ViewData 和 Renderer 是相等的
用法上来说可以这样来看:类型 Renderer<T, VD : LayoutViewData<T>> 可以被等价看待为 LayoutRenderer<T>, 相同的道理, 由于 Updater 和 ViewData 也是一一对应的关系, 因此类型 Updater<T, VD : LayoutViewData<T>> 可以被等价看待为 LayoutUpdater<T>
因此类型 LayoutViewData<T> 可以看做 LayoutXX 系列所有类的一个特征类型:通过这个类型可以唯一地识别出其对应的原始类型
在高阶类型的 Java 类型系统实现中也使用了类似的手法:
可以注意到 VIewData 的原始定义中继承了 ViewDataOf 类型,这个类型原始定义是这样的:
class ForViewData private constructor() { companion object}
typealias ViewDataOf<T> = Kind<ForViewData, T>
@Suppress(“UNCHECKED_CAST”, “NOTHING_TO_INLINE”)
inline fun <T> ViewDataOf<T>.fix(): ViewData<T> =
this as ViewData<T>
其中 ForViewData 就是我们定义的特征类型,在 fix() 函数中我们通过识别 Kind<ForViewData, T> 中的 ForViewData 部分即可识别为其唯一对应的 ViewData<T>,从而安全地进行类型转换
注意,特征类型只是用于类型系统识别用,代码中我们实际并不会使用它的实例,因此可以看到上面定义的 ForViewData 类型是私有构造函数,无法被实例化
另外,实际只要是具有唯一性任意类型都可以作为特征类型,可以利用已有的类型(比如 LayoutViewData)、也可以单独定义(比如 ForViewData)
利用特征类型我们可以更灵活使用类型(见 Kotlin 与高阶类型),也可以简化冗余的类型信息
比如之前版本的 DslAdapter 中,Adapter 的泛型信息为:<T, VD : ViewData<T>, BR : BaseRenderer<T, VD>>
包含了 T、VD 和 BR 三个类型信息,但根据我们上面的分析,VD 和 BR 两个类型实际是等价的,他们包含了相同的类型特征,因此我们可以将其简化为 <T, VD : ViewData<T>,两个泛型即可保留所有我们需要的类型信息
对于复杂的 Adapter 这种简化对于减少编译系统压力来说的是更加明显的:
比如原来的类型有 6000 多字符:
ComposeRenderer<HConsK<ForIdT, Pair<ED, SearchConditions>, HConsK<ForIdT, Pair<ED, SearchConditions>, HNilK<ForIdT>>>, HConsK<ForComposeItem, Pair<Pair<ED, SearchConditions>, SealedItemRenderer<Pair<ED, SearchConditions>, HConsK<Kind<ForSealedItem, Pair<ED, SearchConditions>>, Pair<List<Pair<ED, CategoryInfo>>, ListRenderer<List<Pair<ED, CategoryInfo>>, Pair<ED, CategoryInfo>, MapperViewData<Pair<ED, CategoryInfo>, HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, ComposeViewData<HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, HConsK<ForComposeItemData, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItemData, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItemData>>>>>, MapperRenderer<Pair<ED, CategoryInfo>, HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, ComposeViewData<HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, HConsK<ForComposeItemData, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItemData, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItemData>>>>, ComposeRenderer<HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, HConsK<ForComposeItem, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItem, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItem>>>, HConsK<ForComposeItemData, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItemData, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItemData>>>>>>>, HConsK<Kind<ForSealedItem, Pair<ED, SearchConditions>>, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HNilK<Kind<ForSealedItem, Pair<ED, SearchConditions>>>>>>>, HConsK<ForComposeItem, Pair<Pair<ED, SearchConditions>, MapperRenderer<Pair<ED, SearchConditions>, List<Pair<ED, NormalCategoryInfo>>, ListViewData<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>>, HNilK<ForComposeItem>>>, HConsK<ForComposeItemData, Pair<Pair<ED, SearchConditions>, SealedItemRenderer<Pair<ED, SearchConditions>, HConsK<Kind<ForSealedItem, Pair<ED, SearchConditions>>, Pair<List<Pair<ED, CategoryInfo>>, ListRenderer<List<Pair<ED, CategoryInfo>>, Pair<ED, CategoryInfo>, MapperViewData<Pair<ED, CategoryInfo>, HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, ComposeViewData<HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, HConsK<ForComposeItemData, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItemData, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItemData>>>>>, MapperRenderer<Pair<ED, CategoryInfo>, HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, ComposeViewData<HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, HConsK<ForComposeItemData, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItemData, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItemData>>>>, ComposeRenderer<HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, HConsK<ForComposeItem, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItem, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItem>>>, HConsK<ForComposeItemData, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HConsK<ForComposeItemData, Pair<CategoryInfo, SealedItemRenderer<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyRenderer<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingRenderer<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItemData>>>>>>>, HConsK<Kind<ForSealedItem, Pair<ED, SearchConditions>>, Pair<List<Pair<ED, NormalCategoryInfo>>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>, HNilK<Kind<ForSealedItem, Pair<ED, SearchConditions>>>>>>>, HConsK<ForComposeItemData, Pair<Pair<ED, SearchConditions>, MapperRenderer<Pair<ED, SearchConditions>, List<Pair<ED, NormalCategoryInfo>>, ListViewData<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD>, ListRenderer<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD, BR>>>, HNilK<ForComposeItemData>>>>
简化后只有 1900 多个字符:
ComposeRenderer<HConsK<ForIdT, Pair<ED, SearchConditions>, HConsK<ForIdT, Pair<ED, SearchConditions>, HNilK<ForIdT>>>, HConsK<ForComposeItemData, Pair<Pair<ED, SearchConditions>, SealedViewData<Pair<ED, SearchConditions>, HConsK<Kind<ForSealedItem, Pair<ED, SearchConditions>>, Pair<List<Pair<ED, CategoryInfo>>, ListViewData<List<Pair<ED, CategoryInfo>>, Pair<ED, CategoryInfo>, MapperViewData<Pair<ED, CategoryInfo>, HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, ComposeViewData<HConsK<ForIdT, List<Pair<ED, NormalCategoryInfo>>, HConsK<ForIdT, CategoryInfo, HNilK<ForIdT>>>, HConsK<ForComposeItemData, Pair<List<Pair<ED, NormalCategoryInfo>>, ListViewData<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD>>, HConsK<ForComposeItemData, Pair<CategoryInfo, SealedViewData<CategoryInfo, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<Unit, EmptyViewData<Unit>>, HConsK<Kind<ForSealedItem, CategoryInfo>, Pair<CategoryInfo, DataBindingViewData<CategoryInfo, CategoryInfo>>, HNilK<Kind<ForSealedItem, CategoryInfo>>>>>>, HNilK<ForComposeItemData>>>>>>>, HConsK<Kind<ForSealedItem, Pair<ED, SearchConditions>>, Pair<List<Pair<ED, NormalCategoryInfo>>, ListViewData<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD>>, HNilK<Kind<ForSealedItem, Pair<ED, SearchConditions>>>>>>>, HConsK<ForComposeItemData, Pair<Pair<ED, SearchConditions>, MapperViewData<Pair<ED, SearchConditions>, List<Pair<ED, NormalCategoryInfo>>, ListViewData<List<Pair<ED, NormalCategoryInfo>>, Pair<ED, NormalCategoryInfo>, VD>>>, HNilK<ForComposeItemData>>>>
递归类型
函数式列表
函数范式中有 List 类型,但与 OOP 中的列表的结构是完全不同的:
List a = Nil | Cons a (List a)
或者用 kotlin 来描述为:
sealed class List<A>
object Nil : List<Nothing>
data class Cons<T>(val head: T, val tail: List<T>) : List<T>
它可以看做是一个自包含的数据结构:如果没有数据就是 Nil;如果有数据则包含一个数据以及下一个 List<T>,直到取到 Nil 结束
即递归数据结构
异构列表
强类型化最困难的在于 ComposeRenderer 的强类型化,ComposeRenderer 可以实现的是组合不同的 Renderer:
|– item1 (eg: itemRenderer)
|– item2 (eg: stringRenderer)
val composeRenderer = ComposeRenderer.startBuild
.add(itemRenderer)
.add(stringRenderer)
.build()
原始的实现方法可以通过一个 List 来保存:
List<BaseRenderer>
但这样就丢失了每个元素的类型特征信息,比如我们无法知道第二个元素是 stringRenderer 还是 itemRenderer,这也是现有所有库都存在的问题,常常我们只能使用强制类型转换的方式来得到我们希望的类型,但没有类型系统的检查这种转换是不安全的,只能在运行期被检查出来
无论是 OOP 的列表还是上面介绍的函数式列表都无法满足这个需求,他们在 add() 的时候都把类型特征丢弃了
而异构列表可以将这些保留下来:
sealed class HList
object HNil : HList()
data class HCons<out H, out T : HList>(val head: H, val tail: T) : HList()
可以看到它的数据结构和原始的函数式列表的结构很相似,都是递归数据结构
但它在泛型中增加了 out T : HList,这是一个引用了自己类型的泛型声明,即:
HList = HCons<T1, HList>
HList = HCons<T1, HCons<T2, HList>>
HList = HCons<T1, HCons<T2, HCons<T3, HList>>>
HList = HCons<T1, HCons<T2, HCons<T3, HCons<T4, HList>>>>
…
它的类型可以在不断的代换中形成类型列表,这种即是递归类型
使用上:
// 原函数
fun test2(s: String, i: Int): List<Any?> = listOf(s, i)
// 异构列表
fun test2(s: String, i: Int): HCons<Int, HCons<String, HNil>> =
HCons(i, HCons(s, HNil))
同样是构建列表, 异构列表包含了更丰富的类型信息:
容器的 size 为 2
容器中第一个元素为 String, 第二个为 Int
相比传统列表,异构列表的优势:
完整保存所有元素的类型信息
自带容器的 size 信息
完整保存每个元素的位置信息
这是基本的异构列表,DslAdapter 为了做类型限定而自定义了高阶异构列表,可以参考源码 HListK.kt
递归类型
递归类型是指的包含有自己的类型声明:
fun <DL : HListK<ForIdT, DL>> test() = …
这种泛型可以在不断代换中形成上面描述的类型列表:
HList = HCons<T1, HList>
HList = HCons<T1, HCons<T2, HList>>
HList = HCons<T1, HCons<T2, HCons<T3, HList>>>
HList = HCons<T1, HCons<T2, HCons<T3, HCons<T4, HList>>>>
…
在描述数量不确定的类型时很有用
辅助类型
在使用 DslAdapter 中可能会注意到有时会有一个奇特的参数 type:
fun <T, D, VD : ViewData<D>>
BaseRenderer<D, VD>.mapT(type: TypeCheck<T>,
mapper: (T) -> D,
demapper: (oldData: T, newMapData: D) -> T)
: MapperRenderer<T, D, VD> = …
这个参数的实际值并不会在函数中被使用到,而跳转到 TypeCheck 的定义中:
class TypeCheck<T>
private val typeFake = TypeCheck<Nothing>()
@Suppress(“UNCHECKED_CAST”)
fun <T> type(): TypeCheck<T> = typeFake as TypeCheck<T>
TypeCheck 只是一个没有任何功能的空类型,返回的值也是固定的同一个值,那这个参数究竟是用来干什么的?
要解释这个问题我们需要看一下 mapT 这个函数的调用:
LayoutRenderer<String>(MOCK_LAYOUT_RES, 3)
.mapT(type = type<TestModel>(),
mapper = {it.msg},
demapper = {oldData, newMapData -> oldData.copy(msg = newMapData) })
上面这段代码的作用是将接收 String 数据类型的 Renderer 转换为接受 TestModel 数据类型
如果我们不使用 type 这个参数的话这段代码会变成什么样呢:
LayoutRenderer<String>(MOCK_LAYOUT_RES, 3)
.map<TestModel, String, LayoutViewData<String>>(
mapper = {it.msg},
demapper = {oldData, newMapData -> oldData.copy(msg = newMapData) })
可以看到由于函数的第一个泛型 T 类型系统也无法推测出来为 TestModel,因此需要我们显式地把 T 的类型给写出来,但 kotlin 和 Java 的语法中无法只写泛型声明中的一项,所以我们不得不把其他可以被推测出来的泛型也显式的声明出来,不仅代码繁杂而且在类型复杂的时候几乎是不可完成的工作
参数 type 中的泛型 <T> 就可以用于辅助编译器进行类型推测,我们只需要手动声明编译器难以自动推测的部分泛型,而其他可以被推测的泛型还是交由编译器自动完成,即可简化代码
而这里的 type = type<TestModel>() 即是辅助类型,它的实例本身没有任何作用,它只是为了辅助编译器进行类型检查
最后
DslAdapter 致力于完整的静态类型,使用了各种类型编程的技法,这是因为足够的类型信息不仅能帮编译器进行类型检查、早期排除问题,而且能够帮助我们在编码的时候编写足够准确的代码
准确的代码意味着我们即不需要处理不会发生的额外情况、也不会少处理了可能的情况。(早期版本的 DslAdapter 的更新模块就是被设计为自动检查更新,导致需要处理大量额外情况,极其不安全)
同时 DslAdapter 本身不是一个希望做到全能的库,它的目标是将 Adapter 的工作足够简化,并只专注于 Adapter 工作,其他功能就交给专注其他功能的库
Do One Thing and Do It Well.
但这并不意味着 DslAdapter 是一个抛弃了功能性的库,相反,它是一个极其灵活的库。它的核心被设计得非常简单,只有 BaseRenderer 和 RendererAdapter,这两个类也相当简单,并且由于全局只有一个变量被保存在 RendererAdapter 中,保证了数据的线程安全性。而数据中都是不可变属性,使内部数据也可以被完全暴露出来,方便了功能的扩展
实际上 DSL 更新器 dsladapter-updater 模块以及 DSL position 获取器 dsladapter-position 模块都是以扩展方式存在的,后续还会根据需求继续扩展其他模块
本文只是浅尝则止地介绍了一点 DslAdapter 的开发技巧,欢迎 Star 和提出 issue