Android开发Android开发经验谈Android技术知识

Kotlin扩展函数 + DSL 封装Adapter,是时候提高

2020-07-16  本文已影响0人  ea14cffb33a4

前言

在项目开发中,总离不开列表,说到列表,就会有无穷无尽的 Adapter 需要你去实现。从而出现了很多优秀的 adapter 库。

不过很多库功能都很全面了,有很多个类,很多功能,但是实际上只用其中的一两个我感觉是很多人的现状。所以自己实现一个使用起来顺手的是一个不错的选择,特别是用在一些不是很复杂的列表时。

效果

先看看使用效果,激发一下你为数不多继续阅读的激情:

//单类型列表,默认 LinearLayoutManager
recycleView.setup<NumberInfo> {
    dataSource(initData())
    adapter {
        addItem(R.layout.layout_item) {
            bindViewHolder { data, _, _ ->
                setText(R.id.number, data?.number.toString())
            }
        }
    }
}

//多类型列表
recycleView.setup<Any> {
    withLayoutManager { LinearLayoutManager(context) }
    dataSource(data)
    adapter {
        addItem(R.layout.item_setion_header) {
            isForViewType { data, _ -> data is SectionHeader }
            bindViewHolder { data, _, _ ->
                val header = data as SectionHeader
                setText(R.id.section_title, header.title)
            }
        }
        addItem(R.layout.item_user) {
            isForViewType { data, _ -> data is User }
            bindViewHolder { data, _, _ ->
                val user = data as User
                setText(R.id.name, user.name)
                setImageResource(R.id.avatar, user.avatarRes)
                //如果你的控件找不到方便赋值的方法,可以通过 findViewById 去查找
                val phone = findViewById<TextView>(R.id.phone)
                phone.text = user.phone
            }
        }
    }
}

嗯....,感觉还可以,最少的情况下可以把一个列表代码用 10 几行就完成了。

完整代码地址

先贴完整代码地址,没地址的文章是没灵魂的:EfficientAdapterhttps://github.com/EspoirX/EfficientAdapter , 我把它命名为 EfficientAdapter ,意为高效的意思,事实上它只有 3 个文件。 至于如何使用,在地址上已经描述了,所以这篇文章主要是讲一下实现的思路。

实现思路

对 Adapter 的封装,其实无非就是对 Adapter 里面的几个回调方法进行封装罢了,最常用的方法是先定义好一个存放 ViewHolder 的列表,然后在各个回调中获取这些 ViewHolder,然后实现逻辑。

那么其中最操蛋的是哪个回调方法的封装呢?我认为是 getItemViewType。事实上你可以在很多框架中看到让你实现获取 ViewType 的回调方法。

一步一步来,先说ViewHolder 的封装 在 EfficientAdapter 里面,我把 ViewHolder 的封装写成了 BaseViewHolder

class BaseViewHolder(parent: ViewGroup, resource: Int) : RecyclerView.ViewHolder(
        LayoutInflater.from(parent.context).inflate(resource, parent, false)
)

这就是我的封装,够简单吧。

想什么呢,当然没这么简单,想要在上面使用效果代码中那样实现 ViewHolder 的具体逻辑,还需要有 isForViewTypebindViewHolder 等方法。所以我要定义一个类,去提供这些方法:

abstract class ViewHolderCreator<T> {
    abstract fun isForViewType(data: T?, position: Int): Boolean
    abstract fun getResourceId(): Int
    abstract fun onBindViewHolder(
            data: T?, items: MutableList<T>?,
            position: Int,  holder: ViewHolderCreator<T>
    )

    var itemView: View? = null

    fun registerItemView(itemView: View?) {
        this.itemView = itemView
    }

    fun <V : View> findViewById(viewId: Int): V {
        checkItemView()
        return itemView!!.findViewById(viewId)
    }

    private fun checkItemView() {
        if (itemView == null) {
            throw NullPointerException("itemView is null")
        }
    }
}

ViewHolderCreator 中,getResourceIdonBindViewHolder 方法相信都知道是干嘛的,而 isForViewType 方法是用来判断 ViewType 的,注意它返回类型是 Boolean,这个方法会在下面讲到。因为我想在 onBindViewHolder 中能方便的拿到 view,所以有了 registerItemViewfindViewById 等其他方法。

以上就是 ViewHolder 的所有封装,接下来就对 Adapter 的封装。

open class EfficientAdapter<T> : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
    var items: MutableList<T>? = mutableListOf()
    private val typeHolders: SparseArrayCompat<ViewHolderCreator<T>> = SparseArrayCompat()
}

Adapter 首先需要一个泛型用来表示传入的实体类类型,定义了一个 item 列表用来做数据源。ViewHolder 的集合使用一个 SparseArrayCompat 去存储。之所以用 SparseArray ,是因为我想把 ViewType 做为 key

所以,在 onCreateViewHolder 回调方法中,需要根据 viewType 参数在 typeHolders 中取到具体的 ViewHolderCreator

private fun getHolderForViewType(viewType: Int): ViewHolderCreator<T>? {
    return typeHolders.get(viewType)
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    val holder = getHolderForViewType(viewType)
            ?: throw NullPointerException("No Holder added for ViewType $viewType")
    return BaseViewHolder(parent, holder.getResourceId())
}

这样,就可以通过 getHolderForViewType方法,在 typeHolders 中获取到对应的 ViewHolderCreator,然后根据 ViewHolderCreator 中的信息去创建一个新的 ViewHolder。如果找不到,就抛一个空指针异常。

同样道理,onBindViewHolder 回调方法也可以这么做:

override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int) {
    onBindViewHolder(viewHolder, position, mutableListOf())
}

override fun onBindViewHolder(viewHolder: RecyclerView.ViewHolder, position: Int, payloads:MutableList<Any>) {
    val holder = getHolderForViewType(viewHolder.itemViewType)
            ?: throw NullPointerException("No Holder added for ViewType " + viewHolder.itemViewType)
    holder.registerItemView(viewHolder.itemView)
    holder.onBindViewHolder(items?.get(position), items, position, holder)
}

注意的是 onBindViewHolder 回调方法有两个,他们的区别就不说了,这里两个都实现了逻辑,当然你也可以只实现一个。

还剩下 getItemCountgetItemViewType 回调方法了,getItemCount 其实没什么好说的:

override fun getItemCount(): Int = items?.size ?: 0

先不说如何实现 getItemViewType ,先说说怎么添加数据到 typeHolders中:

fun register(holder: ViewHolderCreator<T>) = apply {
    var viewType: Int = typeHolders.size()
    while (typeHolders.get(viewType) != null) {
        viewType++
    }
    typeHolders.put(viewType, holder)
}

typeHolders 的类型是 SparseArrayCompat,这里我用 ViewType 作为 key,register 方法中,可以看到没每注册一次,viewType 就自动加一(因为 typeHolders 的长度会变长),达到了不会重复的效果,到时候在实现 getItemViewType 的时候,就直接取出来即可。避免了具体业务的干扰。

最后看看 getItemViewType的实现:

override fun getItemViewType(position: Int): Int {
    if (items == null) {
        throw NullPointerException("adapter data source is null")
    }
    for (i in 0 until typeHolders.size()) {
        val holder = typeHolders.valueAt(i)
        val data = items?.getOrNull(position)
        if (holder.isForViewType(data, position)) {
            return typeHolders.keyAt(i)
        }
    }

    //找不到匹配的 viewType
    throw NullPointerException(
            "No holder added that matches at position=$position in data source")
}


该方法的思路是通过遍历 typeHolders,通过 ViewHolderCreatorisForViewType 方法来判断是否符合条件,如果符合,则在 typeHolders 中取出viewType 出来返回。

因为 typeHolders 中的 viewType 是自增的,所以 getItemViewType 的返回值会是 0,1,2,3...

isForViewType 在实际中如何实现?

举个例子:

如果你的数据源由多个实体类组成,比如:

private List<Object> data = new ArrayList<>();
data.add(new User("Marry", 17, R.drawable.icon2, "123456789XX"));
data.add(new SectionHeader("My Images"));
data.add(new Image(R.drawable.cover1));

那么在构建 EfficientAdapter 时,泛型传入的自然是 Object,然后在 isForViewType方法中你可以这样区分类型:

// 代表这是 User 类型
 public boolean isForViewType(Object data, int position) {
   return data instanceof User;
}

 // 代表这是 SectionHeader 类型
 public boolean isForViewType(Object data, int position) {
   return data instanceof SectionHeader;
}

 // 代表这是 Image 类型
 public boolean isForViewType(Object data, int position) {
   return data instanceof Image;
}

如果你的数据源只有一个实体类,但是实体类里面有某个字段可以区分类型,你可以这样:

// 代表这是 User 类型
 public boolean isForViewType(ListInfo data, int position) {
   return data.type = ListInfo.USER
}

 // 代表这是 SectionHeader 类型
 public boolean isForViewType(ListInfo data, int position) {
   return data.type = ListInfo.HEADER
}

 // 代表这是 Image 类型
 public boolean isForViewType(ListInfo data, int position) {
   return data.type = ListInfo.IMAGE
}

其他情况可以根据具体的情况而定。

到这里,已经完成 Adapter 的封装了,接下来可以定义一些数据源的增删查改的方法,比如:

//绑定 RecyclerView
fun attach(recyclerView: RecyclerView) = apply { recyclerView.adapter = this }

//提交数据
fun submitList(list: MutableList<T>) {
    this.items?.clear()
    this.items?.addAll(list)
    notifyDataSetChanged()
}

到这里,已经可以简单粗暴的使用了:

adapter = EfficientAdapter<SectionHeader>()
        .register(object : ViewHolderCreator<SectionHeader>() {
            override fun isForViewType(data: SectionHeader?, position: Int) = data != null
            override fun getResourceId() = R.layout.item_setion_header

            override fun onBindViewHolder(
                    data: SectionHeader?,
                    items: MutableList<SectionHeader>?, position: Int,
                    holder: ViewHolderCreator<SectionHeader>
            ) {
                setText(R.id.section_title, data.title)
            }
        }).attach(recycle_view)
adapter?.submitList(data)


但和使用效果差别有点大啊。所以,接下来就是 kotlin 发挥的时候了。

扩展函数 与 DSL

相信学过 kotlin 的都知道这两个东西,他们可以为我们的代码提供更多的可能。

ViewHolderCreator DSL

由于 ViewHolderCreator 是一个抽象类,对它进行 DSL 封装需要一个默认的实现类(也许可以直接封装,但是我只能想到这种方法):

class ViewHolderDsl<T>(private val resourceId: Int) : ViewHolderCreator<T>() {
    private var viewType: ((data: T?, position: Int) -> Boolean)? = null
    private var viewHolder: ((data: T?, position: Int, holder: ViewHolderCreator<T>) -> Unit)? = null

    fun isForViewType(viewType: (data: T?, position: Int) -> Boolean) {
        this.viewType = viewType
    }

    fun bindViewHolder(holder: (data: T?, position: Int, holder: ViewHolderCreator<T>) -> Unit) {
        viewHolder = holder
    }

    override fun isForViewType(data: T?, position: Int): Boolean {
        return viewType?.invoke(data) ?: (data != null)
    }

    override fun getResourceId() = resourceId

    override fun onBindViewHolder(
            data: T?, items: MutableList<T>?, position: Int, holder: ViewHolderCreator<T>,
            payloads: MutableList<Any>
    ) {
        viewHolder?.invoke(data, position, holder)
    }
}


代码比较清晰,就是对三个抽象方法的实现。由于 getResourceId 比较简单,所以直接放在构造方法中传值就好。

实现好了 ViewHolderDsl,我们给 EfficientAdapter 定义一个扩展函数,用 DSL 的方式去调用 register 方法:

fun <T : Any> EfficientAdapter<T>.addItem(resourceId: Int, init: ViewHolderDsl<T>.() -> Unit) {
    val holder = ViewHolderDsl<T>(resourceId)
    holder.init()
    register(holder)
}

比较简单,就是创建好 ViewHolderDsl 后,调用 register 方法即可。

到这里,其实已经可以用了,只要我们再写一个函数,用 DSL 的方式创建 Adapter 即可:

fun <T : Any> efficientAdapter(init: EfficientAdapter<T>.() -> Unit): EfficientAdapter<T> {
    val adapter = EfficientAdapter<T>()
    adapter.init()
    return adapter
}

所以上面那个简单粗暴的示例代码就可以变成这样:

adapter = efficientAdapter<Any> {
    addItem(R.layout.item_setion_header) {
        isForViewType { it != null }
        bindViewHolder { data, _, _ ->
            setText(R.id.section_title, data.title)
        }
    }
}.attach(recycle_view)
adapter?.submitList(data)

代码又清晰和简单了很多。由于在 ViewHolderDsl 中,isForViewType 的默认实现是 data!=null,所以如果是单类型列表,这个方法可以直接不写。

虽然代码简单了很多,但这样总要定义 adapter 对象和绑定 RecycleView,所以更加优雅的方式就是给 RecycleView 定义一个扩展函数,把这些操作都包装起来。 首先我们实现一个叫 RecycleSetup 的类,在这个类里面,把 RecycleView 的配置以及 Adapter 操作,数据源操作等通通包装起来:

class RecycleSetup<T> internal constructor(private val recyclerView: RecyclerView) {

    var items = mutableListOf<T>()
    var adapter: EfficientAdapter<T>? = null
    var context = recyclerView.context

    fun dataSource(items: MutableList<T>) {
        this.items.clear()
        this.items = items
    }

    fun withLayoutManager(init: RecycleSetup<T>.() -> RecyclerView.LayoutManager) =
            apply { recyclerView.layoutManager = init() }

    fun adapter(init: EfficientAdapter<T>.() -> Unit) {
        this.adapter = EfficientAdapter()
        init.invoke(adapter!!)
        recyclerView.adapter = adapter
        adapter?.submitList(this.items)
    }

    fun submitList(list: MutableList<T>) {
        this.items.clear()
        this.items = list
        adapter?.submitList(this.items)
    }

    fun getItem(position: Int): T = items[position]
}

代码简单,相信大家都能看懂。

有了这个类,最后,就可以给 RecycleView 实现扩展函数了:

fun <T> RecyclerView.setup(block: RecycleSetup<T>.() -> Unit): RecycleSetup<T> {
    val setup = RecycleSetup<T>(this).apply(block)
    if (layoutManager == null) {
        layoutManager = LinearLayoutManager(context)
    }
    return setup
}

fun <T> RecyclerView.submitList(items: MutableList<T>) {
    if (adapter != null && adapter is EfficientAdapter<*>) {
        (adapter as EfficientAdapter<T>).submitList(items)
    }
}

layoutManager 为空就默认实现 LinearLayoutManager。最后,上面那个简单粗暴的代码就可以写成跟一开始说那个效果一样了:

recycleView.setup<SectionHeader> {
    adapter {
        addItem(R.layout.item_setion_header) {
            bindViewHolder { data, _, _ ->
                setText(R.id.section_title, data.title)
            }
        }
    }
}
recycleView.submitList(data)

完整代码和例子都在这里 EfficientAdapter ,有兴趣可以看看。

总结

其实,整篇文章的代码思路都比较简单,其中比较有意思的是 viewType 自加一这里,在使用的时候用户只需要实现 isForViewType 即可,这可以避免了你的实体类需要继承某一个 Base 类。

当然相比各个大佬们的库,这个算是比较简单的,所以写这篇文章的原因是分享自己在封装代码的时候的一个思路,一步一步从零到有。相信很多人都需要这种东西,比整天搬砖有意思,也会学到一点点知识。

Thank you for 看完整篇文章!

文章最后 分享

移动架构师笔记+阿里P6P7【安卓】进阶资料分享+加薪跳槽必备面试题+安卓程序员简历模板

上一篇下一篇

猜你喜欢

热点阅读