Android用DSL实现复杂RecyclerView的思路分析

阅读本文需要您对 DSL, Kotlin, DataBinding 有一定的了解,阅读时长约 8分钟

ReactiveX

响应式编程是一种面向数据流和变化传播的编程范式。随着自己知识领域的逐渐深入,我越来越依赖 RxJava。在Java语言中,通过lambda和方法引用,配合RxJava额外提供的函数式接口,链式调用的代码写起来既优美又有逼格

RxJava的毒性令人欲罢不能,我趋之若鹜,尝试在自己的 Github 上做出了很多Rx相关工具的尝试(比如这个)。不久前,在 Yumenokanata 的影响下,我尝试实现一个基于RxJava的Dialog库 RxDialog,以对应原生Dialog的一些行为(创建与显示,以及事件监听等等)。

困惑

很快,我遇到的问题是,我无法控制库对Dialog控制的粒度。举例来说,目前RxDialog的效果是,通过配置一些简单的参数,用Kotlin不同于Java的Builder模式,创建出一个标准的AlertDialog,并展示在界面上,而Dialog中不同的事件(点击事件,dismiss事件)则作为流被Observable通知给观察者:

button.setOnClickListener {
         RxDialog
                .build(this) {
                    title = "I am title"
                    message = "I am message"
                    buttons = arrayOf(
                            EventType.CALLBACK_TYPE_POSITIVE,
                            EventType.CALLBACK_TYPE_NEGATIVE,
                            EventType.CALLBACK_TYPE_DISMISS
                    )
                    positiveText = getString(R.string.static_dialog_button_ok)
                    positiveTextColor = R.color.positive_color
                    negativeText = getString(R.string.static_dialog_button_cancel)
                    negativeTextColor = R.color.negative_color
                    cancelable = false
                }
                .subscribe { event ->
                      when (event.button) {
                          EventType.CALLBACK_TYPE_POSITIVE -> {
                              toast("click the OK")
                          }
                          EventType.CALLBACK_TYPE_NEGATIVE -> {
                              toast("click the CANCEL")
                          }
                          EventType.CALLBACK_TYPE_DISMISS -> {
                              toast("dismiss...")
                          }
                      }
                }
}

看起来不错,作为一个工具类还凑活,但是如果面对一些自定义UI的Dialog,RxDialog就无能为力了(尴尬的是,对于产品而言,这种情况几乎是必然发生)。

同样,如果这个Dialog的内容是一个动态的列表时,RxDialog依然束手无策,简单的策略是像原生API一样,通过 AlertDialog.Builder(context).setAdapter()来实例化一个列表Dialog,但是原生的API内部实际上是实例化了一个ListView而不是RecyclerView,我不是很想这样做。

我不知道该怎么把握这个库对于Dialog UI控制的粒度,于是这个库停工了。在重新启动之前,我准备寻求一些优秀源码,从它们的设计思想中获取帮助。

RecyclerView似乎和Dialog很相似,我注意到了这样一个工具库——
DslAdapter,它是上文说到的,Yumenokanata 自己写的一个工具。

DslAdapter

DslAdapter:A RecyclerView Adapter builder by DSL. Easy to use, and all code written by kotlin.

DslAdapter是一个RecyclerView Adapter的工具库,它基于Kotlin,简单易用(老实讲,个人看来不是很好上手...),关键是,你可以用DSL的方式实现列表展示。

关于DSL的解释,请参考 百度百科:领域特定语言,本文不细述。

对于官方给出的sample,它不仅支持简单的列表展示,同时支持 各种复杂的多类型列表(包括Header和Footer),支持DataBinding,支持数据驱动UI的更新自动更新

sample中的代码展示了如何实现一个非常复杂的多类型列表,包括DataBinding和数据更新的功能:

Android用DSL实现复杂RecyclerView的思路分析_第1张图片
sample

现在看来,对于复杂的列表来说,不同类型的Item的展示,如何统一交给RecyclerView去展示,似乎和我遇到不同类型Dialog展示的问题非常相似,我决定尝试理解DslAdapter的设计思路,看看能不能给我带来一些好的想法。

本文不是教程,感兴趣的朋友请以官方sample为准——sample中的代码已经讲述的很清晰了。


思路分析

1.RendererAdapter及其Builder

DslAdapter的设计思路是,将整个列表像搭积木一样组合起来——哪怕它再复杂。对于复杂的列表而言,需要面对的就是对不同类型数据的添加,然后将不同类型的数据展示在不同的ItemLayout上。

当然,不可避免的,RecyclerView.Adapter依然是最重要的一个类,和很多开源库的思路一样,DslAdapter也同样实现了一个 RendererAdapter,它也是RecyclerView.Adapter的一个子类:

class RendererAdapter(builder: Builder) :
      RecyclerView.Adapter() {

}

RendererAdapter的实例化必须依赖一个Builder,我们来看看Builder的代码:

class Builder internal constructor() {
    internal val repositories: MutableList> = ArrayList()

    fun  add(supplier: Supplier,
                               renderer: Renderer): Builder {
        val untypedRepository = supplier as Supplier
        repositories.add(Repo(untypedRepository, renderer as Renderer, false))
        return this
    }

    fun  addItem(item: T,
                                   renderer: Renderer): Builder {
        repositories.add(Repo({ item as Any }, renderer as Renderer, true))
        return this
    }

    fun  addStaticItem(renderer: Renderer): Builder {
        repositories.add(Repo({ Unit }, renderer as Renderer, true))
        return this
    }

    fun build(): RendererAdapter {
        return RendererAdapter(this)
    }
}

data class Repo(val supplier: Supplier, val renderer: Renderer, val isStatic: Boolean)

当一个对象的实例化需要多个参数时,建造者模式(Builder)已被认可为非常好的实现方式之一。复杂列表中的不同数据便是如此,Adapter的Builder提供了2种不同的Item接口:静态Item (比如Header和Footer,它们的展示不受列表数据源的变化而变化)和数据源对应的 普通列表的Item(一般来讲,数据源中的每一条数据对应一个Item)。

add()函数对应的是普通列表的Item的处理逻辑,每当有一种数据需要被展示,就调用一次add()函数,一般来讲,最简单的列表,只需要调用一次add()函数就够了。

addStaticItem()函数对应的是静态Item,比如说,我想添加一个Footer,只需要调用一次这个方法,作为最后一块的积木拼接在Adapter的最底部就行了。

当然我们看到还有一个addItem()函数,实际上它和add()函数本质是一样的,这个后文会讲到。

2.Supplier提供数据,Renderer处理展示数据

无论是add()函数还是addStaticItem()函数,它们涉及到了非常重要的两个参数:Supplier 和Renderer

Supplier是一个函数,负责提供数据源。

typealias Supplier = () -> T

Renderer则是一个接口,负责将数据响应到UI上:

interface Renderer {
    // 获取单个数据
    fun getData(content: Data): VD

    // 获取ItemId
    fun getItemId(data: VD, index: Int): Long = RecyclerView.NO_ID

    // 获取Item类型
    fun getItemViewType(data: VD, position: Int): Int

    // 创建ViewHolder
    fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder

    // 绑定数据到UI上,你可以理解为onBindViewHolder()
    fun bind(data: VD, index: Int, holder: RecyclerView.ViewHolder)

    // View的回收
    fun recycle(holder: RecyclerView.ViewHolder)

    // 数据的更新
    fun getUpdates(oldData: VD, newData: VD): List
}

interface ViewData {
    val count: Int
}

第一次看到这里,我认为已经抽象的程度已经可以了,至少我研究了一段时间才缓过来。

设计者的想法是,既然RecyclerView.Adapter的作用是,获取数据,并且展示到RecyclerView上面,那么我将RecyclerView.Adapter这两个最重要的功能都抽象出来,这样一来,自己实现的DslAdapter需要做的就只剩下 调度 了。

当然,Renderer不止这么简单,因为根据不同的需求,有很多种不同的Renderer:

Android用DSL实现复杂RecyclerView的思路分析_第2张图片

不同的需求采用不同的策略,也可以选择对应的Renderer,以LayoutRenderer为例:

class LayoutRenderer(
        @LayoutRes val layout: Int,
        val count: Int = 1,
        val binder: (View, T, Int) -> Unit = { _, _, _ -> },
        val recycleFun: (View) -> Unit = { },
        val stableIdForItem: (T, Int) -> Long = { _, _ -> -1L },
        val keyGetter: (T) -> Any? = { it }
) : BaseRenderer>() {

    override fun getData(content: T): LayoutViewData = LayoutViewData(count, content)

    override fun getItemId(data: LayoutViewData, index: Int): Long = stableIdForItem(data.data, index)

    override fun getLayoutResId(data: LayoutViewData, index: Int): Int = layout

    override fun bind(data: LayoutViewData, index: Int, holder: RecyclerView.ViewHolder) {
        binder(holder.itemView, data.data, index)
    }

    override fun recycle(holder: RecyclerView.ViewHolder) {
        recycleFun(holder.itemView)
    }

    override fun getUpdates(oldData: LayoutViewData, newData: LayoutViewData): List {
        if (oldData.count != newData.count)
            throw UnknownError("oldData count is different with newData count: old=$oldData, new=$newData")

        return if (keyGetter(oldData.data) != keyGetter(newData.data))
            listOf(OnRemoved(0, newData.count),
                    OnInserted(0, newData.count))
        else
            if (oldData.data != newData.data)
                listOf(OnChanged(0, newData.count, newData.data))
            else
                emptyList()
    }

    companion object
}

data class LayoutViewData(override val count: Int, val data: T) : ViewData

实例化这样一个LayoutRenderer,需要传入很多的参数,其中大多数的参数都是函数,这就是为什么我们可以通过lambda实现这样的LayoutRenderer,并生成对应的Adapter:

val adapter = RendererAdapter.repositoryAdapter()
     .addStaticItem(layout(R.layout.list_header))
     .add({ provideData(index) },
             LayoutRenderer(layout = R.layout.simple_item,
                     stableIdForItem = { item, index -> item.id },
                     binder = { view, itemModel, index -> view.findViewById(R.id.simple_text_view).text = itemModel.title },
                     recycleFun = { view -> view.findViewById(R.id.simple_text_view).text = "" })
                     .forList({ i, index -> index }))
     .build()

3.RendererAdapter实现的细节

如果说第一眼感觉最难以理解的是Supplier和Render的抽象,实际看进去,比较复杂的恰恰是RendererAdapter对两者的调度(不知道设计者实现这些细节花了多久,个人感觉应该需要认真思考一些时间,否则很多细节很难考虑周全的)。

首先来看init{}中,对Adapter的初始化:

class RendererAdapter(builder: Builder) : RecyclerView.Adapter() {

    private val repositoryCount: Int
    internal val repositories: List>

    private var data: List = emptyList()
        set(value) {
            field = value
            endPositions = value.getEndsPonints()
        }
    private var endPositions: IntArray = intArrayOf()

    init {
        repositories = builder.repositories
        repositoryCount = repositories.size

        data = getCurrentViewData()
    }

    fun getCurrentViewData(): List =
        repositories.map {
            it.renderer.getData(it.supplier())
        }
}

在Builder中,Supplier和Renderer,被放入了一个Repo的对象中,它的作用只是作为一个容器承载Supplier和Renderer,在初始化时,repositories作为所有repo的容器,repositoryCount则记录有几种类型的Item。此外,这几种Item类型对应所有的数据(参考LayoutRenderer,数据被额外包装成了ViewData的子类)都被放入了data中。

当data属性被赋值后,endPositions属性也会随之更新,getEndsPonints()是List的拓展函数:

fun  List.getEndsPonints(): IntArray =
        getEndsPonints { it.count }

fun  List.getEndsPonints(getter: (T) -> Int): IntArray {
    val ends = IntArray(size)
    var lastEndPosition = 0
    for ((i, vd) in withIndex()) {
        lastEndPosition += getter(vd)
        ends[i] = lastEndPosition
    }

    return ends
}

endPositions实际上是对不同类型Item各自最后一条数据,在总的List中的position值记录的一个数组。

举例来说,如果列表中有三种数据,(数据类型-数据数量)分别对应(A-3),(B-4),(C-5),那么这个size为12的endPositions数组值应该是[2,6,11]。

这种行为最初看来难以理解,它的作用是用来通过postion,作为索引检索其他数据。

此外,RendererAdapter还有额外的2个属性:

/**
 * Because the order of function [.getItemViewType] and
 * [.onCreateViewHolder] calls is uncertain,
 * only the last Position will be saved for same type.
 */
private val typeToPositionMap = SparseIntArray()
private val presenterForViewHolder: MutableMap> = HashMap()

这两个属性和上面的endPositions这三个哥们,在最初来看我不了解它们的意义,往下看,它们存在的意义就是——检索。

getItemViewType:如何获取Layout类型

来看看RendererAdapter中对ItemViewType的处理逻辑,以及其内部用到的resolveIndices函数:

override fun getItemViewType(position: Int): Int {
    val (resolvedRepositoryIndex, resolvedItemIndex) = resolveIndices(position, endPositions)
    val type = repositories[resolvedRepositoryIndex].renderer.getItemViewType(
            data[resolvedRepositoryIndex], resolvedItemIndex)
    typeToPositionMap.put(type, position)
    return type
}

// 相对比较复杂,简单了解即可
fun resolveIndices(position: Int, endPos: IntArray): Pair {
    if (position < 0 || position >= endPos.last()) {
        throw IndexOutOfBoundsException(
                "Asked for position $position while count is ${endPos.last()}")
    }

    var arrayIndex = Arrays.binarySearch(endPos, position)
    if (arrayIndex >= 0) {
        do {
            arrayIndex++
        } while (endPos[arrayIndex] == position)
    } else {
        arrayIndex = arrayIndex.inv()
    }

    val resolvedRepositoryIndex = arrayIndex
    val resolvedItemIndex = if (arrayIndex == 0) position else position - endPos[arrayIndex - 1]

    return resolvedRepositoryIndex to resolvedItemIndex
}

getItemViewType需要知道当前Position的Item所对应Layout的类型,以上文的(A-3),(B-4),(C-5)为例,如果position为2,那么, val (resolvedRepositoryIndex, resolvedItemIndex) = Pair(0,2),接下来,就能获取到对应的A数据对应的Renderer,并且通过A数据对应的Renderer获取对应的type。

onCreateViewHolder和onBindViewHolder

override fun onCreateViewHolder(parent: ViewGroup,
                                viewType: Int): RecyclerView.ViewHolder {
    val typeLastPosition = typeToPositionMap.get(viewType)

    val (resolvedRepositoryIndex, _) = resolveIndices(typeLastPosition, endPositions)

    return repositories[resolvedRepositoryIndex].renderer.onCreateViewHolder(parent, viewType)
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    val (resolvedRepositoryIndex, resolvedItemIndex) = resolveIndices(position, endPositions)
    val repo = repositories[resolvedRepositoryIndex]
    presenterForViewHolder[holder] = repo
    repo.renderer.bind(data[resolvedRepositoryIndex], resolvedItemIndex, holder)
}

正如上面的getItemViewType,通过三剑客,我们能检索到position对应Item的Renderer,然后,我们只需要把Adapter需要的行为委托给不同的Renderer处理即可。

到这里,基本的思路已经捋的差不多了,关于DataBinding的支持以及数据的更新,整理好思路,再看对应位置的代码,应该也不难理解。


小结

在经历过思考后,我很钦佩 Yumenokanata 对于 DslAdapter 的设计。学习过程中,我受益匪浅,它的优点在我看来有以下几点(重要程度从低到高):

  • 1.纯Kotlin编写,DSL的Adapter的实现方式;
  • 2.对Kotlin的使用,一些细节之处的代码规范值得学习;
  • 3.Kotlin代码中对函数式思想的应用;
  • 4.设计的思想,对复杂需求的抽象。

上述列表中,在clone代码的最初,我的目标是1和4,所幸我在学习的还同时收获了2,3(对于Kotlin的这些特性,算是查漏补缺把),我在此总结自己的所得,希望对于看到这里的各位能有所帮助。

你可能感兴趣的:(Android用DSL实现复杂RecyclerView的思路分析)