构建Recyclerview DSL

Kotlin2018-12-15 15:31:41

接文章 DSL in action

上一篇文章说了如何把DSL用在项目的布局中,而这篇文章来讲讲怎么把DSL用在Recyclerview中。此框架已经在我的项目中大规模使用,并且极大地提高了Recyclerview列表构建效率和复用能力。

特色

  • 轻量级(只有一个Kotlin文件)

  • 可拓展(你可以完全自定义自己的Item)

  • 易用(它只是对Rec的 OnCreateVH OnBindVH做了代理,不需要额外的学习成本)

  • 写着爽(Anko风格写法,DSL配置列表灵活易用)

看看效果?

这是一个大概的效果,Recyclerview DSL中,我们可以用DSL的风格去配置Item被如何加入到Rec,各个Item的风格是什么样子,具有很大的灵活性和拓展性。

  1. itemManager.refreshAll {

  2.    val books = viewModel.getBooks()

  3.    val bookShelfs = viewModel.getBookShelfs()

  4.    header {

  5.        text = "DSL header"

  6.        color = Color.BLUE

  7.    }

  8.    book.foreach { book ->

  9.        bookItem {

  10.            title = if (book.id != 0) book.title else "Empty Book"

  11.            date = book.returnDate

  12.            url = book.imageUrl

  13.        }

  14.    }

  15.    bookShelfs.foreachIndexed { index, bookShelf ->

  16.        bookShelf {

  17.            title = "Number$index Shelf - ${bookShelf.name}"

  18.            size = bookShelf.size

  19.            url = bookShelf.imageUrl

  20.            onclick {

  21.                startActivity<BookShelfActivity>("id" to bookShelf.id)

  22.            }

  23.        }    

  24.    }

  25.    footer {

  26.        text = "Load More"

  27.        onClick {

  28.            loadMore()

  29.        }

  30.    }

  31. }

核心类概览

  • Item: Recyclerview DSL中,用来保存View对应数据的类,比如说TextView的字符串,Imageview的url等等,基本上可以认为是担任着ViewModel的角色

  • ItemController: 一般内嵌在 Item类的 CompanionObject中,用于代理Item相关的 OnCreateVHOnBindVH逻辑,基本上一个Item的View逻辑和业务逻辑在这里表现。

  • ItemAdapter:Recyclerview DSL所依赖的Adapter,在初始化的时候会用到,后面它很少出面了

  • ItemManager: RecyclerView DSL的Adapter的一个核心成员变量,统管着Adapter的Item和相应的ItemController,比如说他们的刷新,添加,删除。DSL的语法特性拓展,基本上在这里表现。

那怎么用?

  • 定义列表要用的Item(可以全局复用 所以要好好设计)

  • 写一个 MutableList<Item>的拓展

  • 开始使用!

举个栗子?

比如说我要写定义一类Item,这类Item就是一个FrameLayout里面包了个TextView。

然后怎么写呢?

1. 先定义一个Item,我们就叫它 SingleTextItem.kt这个Item里面需要包含一个字符串,将来在 OnBindVH 的代理中传入到 View

  1. /**

  2. * 你自己定义的Item 示例:只有一个Text的Item

  3. */

  4. class SingleTextItem(val content: String) : Item {

  5.   override val controller: ItemController

  6.       get() = TODO("Controller Need")

  7. }

2. 然后我们需要些这类Item对于的逻辑,也就是 ItemController,在伴生对象中进行实现

  1. /**

  2. * 你自己定义的Item 示例:只有一个Text的Item

  3. */

  4. class SingleTextItem(val content: String) : Item {

  5.    /**

  6.     * implements these functions to delegate the core method of RecyclerView's Item

  7.     */

  8.    companion object Controller : ItemController {

  9.        override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder {

  10.            val inflater = parent.context.layoutInflater

  11.            val view = inflater.inflate(R.layout.item_single_text, parent, false)

  12.            val textView = view.findViewById<TextView>(R.id.tv_single_text)

  13.            return ViewHolder(view, textView)

  14.        }

  15.        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: Item) {

  16.            /**

  17.             * 因为Kotlin的智能Cast 所以后面我们就不需要自己强转了

  18.             * DSL 框架可以保证holder和item的对应性

  19.             */

  20.            holder as ViewHolder

  21.            item as SingleTextItem

  22.            /**

  23.             * what you do in OnBindViewHolder in RecyclerView, just do it here

  24.             */

  25.            holder.textView.text = item.content

  26.        }

  27.        /**

  28.         * 在这里声明此Item所对应的ViewHolder,用来从OnCreateViewHolder传View到OnBindViewHolder中。

  29.         * 这个ViewHolder类应该是私有的,只在这里用

  30.         */

  31.        private class ViewHolder(itemView: View?, val textView: TextView) : RecyclerView.ViewHolder(itemView)

  32.    }

  33.    /**

  34.     * 一般来讲,我们把ItemController放在Item的伴生对象里面,不要在这里new ItemController,因为在自动生成ViewType的时候,

  35.     * 我们是根据ItemController::class.java 来建立一一对应关系,如果是new的话,会导致无法相等以至于生成许多ItemType,这样子会严重破坏Recyclerview的缓存机制

  36.     */

  37.    override val controller: ItemController

  38.        get() = Controller

  39. }  

3. 写个拓展函数,来让它支持DSL

  1. /**

  2. * 用DSL来风格来简单保证add SingleTextItem的操作

  3. */

  4. fun MutableList<Item>.singleText(content: String) = add(SingleTextItem(content))

4. 来试试把,用一下~

  1. val recyclerView: RecyclerView = findViewById(R.id.recyclerview)

  2.       recyclerView.layoutManager = LinearLayoutManager(this)

  3.       recyclerView.withItems {

  4.           repeat(10) {

  5.               singleText("this is a single Text: $it")

  6.           }

  7.       }

复杂情景讨论

情景1: 同一个Item下,对于ViewStyle的不同处理

方案:Item中除了必要的数据类,再传入一个 YourView.()->Unit类型的可空 ?闭包。

原理蛮简单,就弄代码了,注释很全…

  1. package cn.edu.twt.retrox.recyclerviewdsldemo

  2. import android.support.v7.widget.RecyclerView

  3. import android.view.View

  4. import android.view.ViewGroup

  5. import android.widget.TextView

  6. import cn.edu.twt.retrox.recyclerviewdsl.Item

  7. import cn.edu.twt.retrox.recyclerviewdsl.ItemController

  8. import org.jetbrains.anko.layoutInflater

  9. /**

  10. * Just do something new with DSL

  11. * we could pass View.() -> Unit

  12. */

  13. class SingleTextItemV2(val content: String, val init: TextView.() -> Unit) : Item {

  14.    /**

  15.     * implements these functions to delegate the core method of RecyclerView's Item

  16.     */

  17.    companion object Controller : ItemController {

  18.        override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder {

  19.            val inflater = parent.context.layoutInflater

  20.            val view = inflater.inflate(R.layout.item_single_text, parent, false)

  21.            val textView = view.findViewById<TextView>(R.id.tv_single_text)

  22.            return ViewHolder(view, textView)

  23.        }

  24.        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: Item) {

  25.            /**

  26.             * 因为Kotlin的智能Cast 所以后面我们就不需要自己强转了

  27.             * DSL 框架可以保证holder和item的对应性

  28.             */

  29.            holder as ViewHolder

  30.            item as SingleTextItemV2

  31.            /**

  32.             * what you do in OnBindViewHolder in RecyclerView, just do it here

  33.             */

  34.            holder.textView.text = item.content

  35.            // custom settings for TextView passed by DSL

  36.            holder.textView.apply(item.init)

  37.        }

  38.        /**

  39.         * 在这里声明此Item所对应的ViewHolder,用来从OnCreateViewHolder传View到OnBindViewHolder中。

  40.         * 这个ViewHolder类应该是私有的,只在这里用

  41.         */

  42.        private class ViewHolder(itemView: View?, val textView: TextView) : RecyclerView.ViewHolder(itemView)

  43.    }

  44.    /**

  45.     * 一般来讲,我们把ItemController放在Item的伴生对象里面,不要在这里new ItemController,因为在自动生成ViewType的时候,

  46.     * 我们是根据ItemController::class.java 来建立一一对应关系,如果是new的话,会导致无法相等以至于生成许多ItemType,这样子会严重破坏Recyclerview的缓存机制

  47.     */

  48.    override val controller: ItemController

  49.        get() = Controller

  50.    override fun areContentsTheSame(newItem: Item): Boolean {

  51.        return newItem is SingleTextItemV2 && content == newItem.content

  52.    }

  53.    override fun areItemsTheSame(newItem: Item): Boolean = this.areContentsTheSame(newItem)

  54. }

  55. /**

  56. * 用DSL来风格来简单保证add SingleTextItem的操作

  57. */

  58. fun MutableList<Item>.advancedText(content: String, init: TextView.() -> Unit) = add(SingleTextItemV2(content, init))

情景2 : 可刷新列表

比如说,分页加载,列表变化,和其他所有可变的Recyclerview列表

方案:这种情况下,我们把 ItemManager拿出来单独操作即可,善用 autorefresh方法和 DiffUtil

  1. lateinit var itemManager: ItemManager

  2. val recyclerView: RecyclerView = findViewById(R.id.recyclerview)

  3. recyclerView.layoutManager = LinearLayoutManager(this)

  4. itemManager = ItemManager()

  5. recyclerView.adapter = ItemAdapter(itemManager)

  6. itemManager.autoRefresh {

  7. // do something here

  8. // see cn.edu.twt.retrox.recyclerviewdsldemo.act.DiffRefreshListAct

  9. }

想要更加好的刷新体验,就要先给给RecyclerviewDSL加入DiffUtil的能力 :

  1. interface Item {

  2.    val controller: ItemController

  3.    fun areItemsTheSame(newItem: Item): Boolean = false

  4.    fun areContentsTheSame(newItem: Item): Boolean = false

  5. }

实现Item接口的时候, 重写后面那俩默认方法即可。 比如说我们要做一个列表,列表里面是一堆文字的item,在最末尾有一个Button,点击Button就会让文字Item添加10个。然后在 autoRefresh的闭包中,我们只需要用DSL来表达这个需求即可。框架会帮我们做这一切。

  1. /**

  2.  * function autoRefresh don't wipe the data of list

  3.  * you should customize the thing needed to do when it refresh (it create a snapshot of list internally and use DiffUtil)

  4.  * in this function : Every Time we refresh , remove the last Button item , then add some Text Item, at Last we add the button at Last

  5.  */

  6. itemManager.autoRefresh {

  7.    if (size > 0 && last() is ButtonItem) removeAt(size - 1) // 如果最后一个是ButtonItem 移除

  8.    val currentSize = size

  9.    repeat(10) {

  10.        advancedText("This is Item : ${currentSize + it}") {

  11.            textSize = if (it > 5) 14f else 18f

  12.        }

  13.    }

  14.    buttonItem("Add Items") { // 添加ButtonItem

  15.        setOnClickListener {

  16.            refreshList()

  17.        }

  18.    }

  19. }

AutoRefresh背后的原理就是,在调用闭包前,对Adapter的Item做一个SnapShot,然后对比AutoRefresh闭包使用之后的ItemList情况,最后使用DiffUtil来处理。

如果你是要对列表进行全量刷新,可以直接使用 refreshll方法,此方法会清除列表然后再添加新的Item,当然这个过程是有DiffUtil参与的。

原理/动机分析

常规开发

如果按照普通的开发流程,构建列表的时候,一般就是 Adapter + List。 Adapter里面包含着ViewHolder的创建和绑定逻辑,这样子在大规模开发迭代中会遇到的一个问题是:Adapter的逻辑越堆积越重,比如说在 OnBindViewHolder方法中包含着重度的业务逻辑, getItemViewType, onCreateViewHolder中包含着大量的样板代码。

  • 定义ViewType常量

  • getItemViewType中各种判断

  • OnCreateViewHolder中做创建

  • OnBindViewHolder做数据绑定

这些代码都会堆积在Adapter中,时间一长,Type一多,Adapter写起来就会很蛋疼。另外, ViewType/ViewHolder/BindViewHolder逻辑都很难去复用,因为他们是写死在ViewHolder里面的。

简单优化一下?

我们开始思考,这些东西是不是可以解耦开呢?

于是你觉得,OnBindViewHolder的逻辑可以写在ViewHolder里面,然后

  1. class CourseViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

  2.    val cardView: CardView = itemView.findViewById(R.id.cv_item_course)

  3.    val textView: TextView = itemView.findViewById(R.id.tv_item_course)

  4.    fun bind(course: Course) {

  5.        // balabalabla

  6.        //各种逻辑各种逻辑

  7.    }

  8. }

  9. // 然后你的OnBindViewHolder方法就简单多了

  10. override fun onBindViewHolder(holder: CourseViewHolder, position: Int) {

  11.    //这里可以用ViewType / holder instance of 来处理多种VH

  12.    val course = courseList[position]

  13.    holder.bind(course)

  14. }

在这种架构下,可以把ViewHolder独立开,解耦一部分Adapter中的逻辑。嗯… 还可以(没啥技术含量)

问题/不足

  • ViewHolder复用问题: 我们只解耦了 OnBindViewHolder的逻辑,但 OnCreateViewHolder还是要再写

  • 复用灵活性问题: 比如说我在复用的时候,Adapter1里面对 CardView要设置1dp的阴影,Adapter2里面需要3dp。 Adapter1里面对这类ViewHolder里面的TextView要设置:字体,颜色,字号。Adapter2里面需要另外的配置。 又比如说,Adapter1里面对于不同地方的同类ViewHolder里面的TextView要设置:字体,颜色,字号等等….

  • ViewType问题: 我们真的需要手动指定ViewType吗,因为经过我的一番思考,ViewType和 ViewHolder::class.java在合理的封装下,可以是1对1的关系。

再次思考 - 到底要怎么解耦?

于是我开始思考在Recyclerview的架构中,确定一类视图到底需要什么?哪些东西可以用一个最小的集合来定义一类视图?

我们来梳理一下:

  1. 展现给用户看的东西 = 视图 + 填充数据

  2. 视图 <- OnCreateViewHolder中相关逻辑

  3. 数据填充 <- OnBindViewHolder中把数据SetView

所以说,只要我们把 OnCreateVH, OnBindVH的逻辑代理出去,就可以把一类Item的视图部分进行完整的解耦。给太子端代码!

  1. interface ItemController {

  2.    fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder // 视图

  3.    fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: Item) // 这里还需要具体实现 -> 视图填充

  4. }

现在我们解耦出了视图,还剩下视图的数据填充。一般来讲,Model数据类型和ViewHolder类型一一对应,因此我们可以认为一种ItemController对应着一个类型的Item(一般就是嵌入的一个data Class)

于是我们把数据类嵌入进去

  1. interface Item {

  2.    val controller: ItemController // 这里应该用companion object

  3. }

比如说我们有一个高度定制的TextView

  1. class IndicatorTextItem(val text: String) : Item {

  2.    private companion object Controller : ItemController {

  3.        override fun onCreateViewHolder(parent: ViewGroup): RecyclerView.ViewHolder {

  4.            val view = parent.context.layoutInflater.inflate(R.layout.schedule_item_indicator, parent, false)

  5.            return IndicatorTextViewHolder(view)

  6.        }

  7.        override fun onBindViewHolder(holder: RecyclerView.ViewHolder, item: Item) {

  8.            holder as IndicatorTextViewHolder

  9.            item as IndicatorTextItem

  10.            holder.indicatorTextView.text = item.text

  11.        }

  12.        private class IndicatorTextViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {

  13.            val indicatorTextView: TextView = itemView.findViewById(R.id.tv_course_indicator)

  14.        }

  15.    }

  16.    override val controller: ItemController get() = Controller

  17. }

在这里,我们就已经把IndicatorTextView这个Recyclerview Item的视图层和数据填充都解耦了出来。只需要塞进去 IndicatorTextItem对象,就可以做到相应的效果。并且这个Item可以在多个Recyclerview Adapter中复用。

Adapter如何协调?

与这套解耦相配合的是一套Adapter的封装,来对接相关的接口完成对应逻辑的解耦已经ViewType的分配

对于Adapter,我们需要完成的逻辑就是 ItemController <----> ViewType的转换。

一个理论前提是:在高度封装的情况下,ViewType并没有具体的语义,它的作用在于区分不同的 ItemController。而对于具体的语义,则转到Item那边来表示,比如说上面的 classIndicatorTextItem(val text:String):Item

落实到方法上:我们可以实现一套 ItemController <----> ViewType的注册机制,那么这套机制的具体需求是什么?应该怎样设计?先列下需求:

  • 一对一的关系 支持相互索引

  • 照顾ViewHolder的全局复用

  • ViewType自动生成

  • 添加Item时自动注册

一对一的关系 支持相互索引:我们可以维护两个Map

  1. // controller to view type

  2. private val c2vt = mutableMapOf<ItemController, Int>()

  3. // view type to controller

  4. private val vt2c = mutableMapOf<Int, ItemController>()

因为要保证Key,Value的相互之前快速索引,因此需要同时管理这两个Map。

添加Item时自动注册 + ViewType自动生成 :Item接口要求必须有一个 controller成员变量,因此在添加到Item List的同时,进行监听。不如来看看代码

  1. object ItemControllerManager {

  2.    private var viewType = 0 // object保证了单例 因此ViewType肯定是从0开始

  3.    // controller to view type

  4.    private val c2vt = mutableMapOf<ItemController, Int>()

  5.    // view type to controller

  6.    private val vt2c = mutableMapOf<Int, ItemController>()

  7.    /**

  8.     * 检查Item(对应的controller)是否已经被注册,如果没有,那就注册一个ViewType

  9.     */

  10.    fun ensureController(item: Item) {

  11.        val controller = item.controller

  12.        if (!c2vt.contains(controller)) {

  13.            c2vt[controller] = viewType

  14.            vt2c[viewType] = controller

  15.            viewType++

  16.        }

  17.    }

  18.    /**

  19.     * 对于一个Collection的ViewType注册,先进行一次去重

  20.     */

  21.    fun ensureControllers(items: Collection<Item>): Unit =

  22.            items.distinctBy(Item::controller).forEach(::ensureController)

  23.    /**

  24.     * 根据ItemController获取对应的Item -> 代理Adapter.getItemViewType

  25.     */

  26.    fun getViewType(controller: ItemController): Int = c2vt[controller]

  27.            ?: throw IllegalStateException("ItemController $controller is not ensured")

  28.    /**

  29.     * 根据ViewType获取ItemController -> 代理OnCreateViewHolder相关逻辑

  30.     */

  31.    fun getController(viewType: Int): ItemController = vt2c[viewType]

  32.            ?: throw IllegalStateException("ItemController $viewType is unused")

  33. }

在Adapter 的数据源修改时,调用相关的 ensureControllers方法来完成相关的注册。同时Adapter中,相关的逻辑也可以被这里的ItemController代理,代码差不多是这样子的:

  1.    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) =

  2.            ItemManager.getController(viewType).onCreateViewHolder(parent)

  3.    override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) =

  4.            itemManager[position].controller.onBindViewHolder(holder, itemManager[position])

在这种情况下,Adapter的两个核心方法就被代理出去了,实现了不同VH逻辑的隔离。

关于自动注册ItemType,我们的做法是实现MutableList接口,内部组合一个普通的MutableList,对 add, addAll, remove之类方法进行AOP处理,这些方法的执行的同时,自动检测或者注册 ItemController,同时对于Adapter进行相应的Notify,这样子就可以实现一个轻量级的MVVM。

在这里,其实我们可以做很多事情,比如说代理出DiffUtil来进行自动Diff

  1. interface Item {

  2.    val controller: ItemController

  3.    fun areItemsTheSame(newItem: Item): Boolean = false

  4.    fun areContentsTheSame(newItem: Item): Boolean = false

  5. }


转载请注明出处:微信公众号 Kotlin


Copyright © 温县电话机虚拟社区@2017