paint-brush
Android 中超级强大的复古列表:委托作为基础经过@6hundreds
330 讀數
330 讀數

Android 中超级强大的复古列表:委托作为基础

经过 Sergey Opivalov9m2023/07/01
Read on Terminal Reader

太長; 讀書

许多项目仍在使用 View 框架,因此使用 RecyclerView 来显示列表。这篇文章是我对 RecylerView 的使用经验的总结。我将讲述我围绕 RecylderView 构建框架的愿景。
featured image - Android 中超级强大的复古列表:委托作为基础
Sergey Opivalov HackerNoon profile picture
0-item
1-item

如今,Jetpack Compose 获得了广泛的采用,而且它绝对值得。这是Android UI 开发中真正的范式转变。那里消除了很多创建列表的样板。而且它很漂亮。


我相信许多项目仍在使用 View 框架,因此使用 RecyclerView 来拥有列表。


据我看来,有以下几个原因:

  • 一些围绕RecyclerView的内部专有解决方案与Compose不容易匹配,因此迁移成本很高
  • 开发人员对 Compose 还不够熟悉
  • 对 Compose 的性能没有信心(还)


但无论如何,Compose 是未来,这就是我在标题中使用“复古”一词的原因。


复古意味着古老但黄金。这篇文章是我对 RecyclerView 的使用经验的总结,对于那些仍在使用 RecyclerView 的人,我将讲述我围绕 RecyclerView 构建框架的愿景。

要求

该框架应该提供合理的可扩展性。因此,主要场景应该用最少量的样板来处理,但如果用户想要创建自定义的东西,它不应该成为障碍。


我将在使用 RecyclerView 期间定义基本场景集,如下所示:

  • 有效更新数据。
    • 众所周知,使用 RecyclerView 期间的最佳实践是使用DiffUtil API 来更新列表中的数据。我们不打算在这里重新发明轮子,因此我们将专注于利用DiffUtil ,并尝试减少其实施的仪式。


  • 定义异构列表。
    • 列表中自然会有多种类型的视图。移动设备的屏幕受到高度的限制,不同的设备具有不同的屏幕尺寸。因此,即使您的内容在一台设备上适合屏幕而不滚动,也可能需要在另一台设备上滚动。


      这就是为什么在 RecyclerView 顶部开发一些屏幕是个好主意。



  • 装饰列表项
    • 装饰是列表中非常常见的情况。尽管您可以将装饰合并到项目布局中,但看起来很简单,但它不是很灵活:相同的项目类型可能需要在不同屏幕上使用不同的装饰,甚至根据列表中项目的位置需要不同的装饰。


基础选项

第一个选择是一切从头开始。它提供了对实施的完全控制,但我们始终可以做到。


有许多第三方开源框架允许使用更少的样板文件来使用 RecyclerView。我不会一一列举,但想用完全相反的方法展示其中两个:


环氧树脂

AirBnb 的解决方案,用于使用 RecyclerView 创建复杂的屏幕。它添加了一堆新的 API,甚至还添加了代码生成,以尽可能减少样板文件。


但我无法摆脱这样的感觉:生成的代码看起来很陌生。此外,注释处理又增加了一层复杂性。


适配器代表

微小的库,实际上提供了一组简洁的函数和接口,仅建议您将异构列表拆分为一组委托适配器的方法。事实上,您可以在相对较短的时间内创建自己的解决方案。


我喜欢小型的本机解决方案,它们足够简单,可以让所有行为触手可及,并且不会隐藏所有实现细节。这就是为什么我相信 AdapterDelagates 及其原则是我们框架基础的良好候选者。

列出项目

最开始的最基本的事情是列表项的通用声明。现实世界中的列表项有很大不同,但我们唯一有信心的是——我们应该能够对其进行比较。


我建议参考DiffUtil.ItemCallback的 API

 interface ListItem { fun isItemTheSame(other: ListItem): Boolean fun isContentTheSame(other: ListItem): Boolean { return this == other } }


这是框架中列表项的简约(因此是可靠的,IMO)声明。方法的语义与 DiffUtil 相同,使用:


isItemTheSame检查otherthis的身份

isContentTheSame检查otherthis中的数据


提供稳定且唯一身份的最常见方法是使用来自服务器的标识符。


因此,让我们有一个抽象实现来减少一些可能的样板代码:

 abstract class DefaultListItem : ListItem { abstract val id: String override fun isItemTheSame(other: ListItem): Boolean = when { other !is DefaultListItem -> false this::class != other::class -> false else -> this.id == other.id } }

如果 ListItem 的实现是一个data class ,则在绝大多数情况下isContentTheSame将非常简单(相等性检查)。


如果列表中的项目不是服务器数据的投影并且没有任何健全的标识,则将ListItem分开仍然是合理的。例如,如果您有页脚和页眉作为列表中的项目,或者您有某种类型的单个项目:


 data class HeaderItem(val title: String) : ListItem { override fun isItemTheSame(other: ListItem): Boolean = other is HeaderItem }


建议的方法使我们能够拥有一个非常自然且单一的DiffUtil回调实现:

 object DefaultDiffUtil : DiffUtil.ItemCallback<ListItem>() { override fun areItemsTheSame(oldItem: ListItem, newItem: ListItem): Boolean = oldItem.isItemTheSame(newItem) override fun areContentsTheSame(oldItem: ListItem, newItem: ListItem): Boolean = oldItem.isContentTheSame(newItem) }


这里的最后一步是声明默认适配器,它从 AdapterDelegates 扩展AsyncListDifferDelegationAdapter

 class CompositeListAdapter : AsyncListDifferDelegationAdapter<ListItem>(DefaultDiffUtil)

异构列表

AdapterDelegates 库提供了一种为特定视图类型声明委托的便捷方法。我们唯一可以改进的是稍微减少样板:

 inline fun <reified I : ListItem, V : ViewBinding> defaultAdapterDelegate( noinline viewBinding: (layoutInflater: LayoutInflater, parent: ViewGroup) -> V, noinline block: AdapterDelegateViewBindingViewHolder<I, V>.() -> Unit ) = adapterDelegateViewBinding<I, ListItem, V>(viewBinding = viewBinding, block = block)


出于展示目的,让我们考虑一个示例,包含一个文本字段的某些TitleListItem的声明可能如下所示:

 data class TitleListItem( override val id: String, val title: String, ) : DefaultListItem()


并委托它

fun titleItemDelegate(onClick: ((String) -> Unit)) = defaultAdapterDelegate< TitleListItem, TitleListItemBinding >(viewBinding = { inflater, root -> TitleListItemBinding.inflate(inflater,root,false) }) { itemView.setOnClickListener { it(item.id) } bind { binding.root.text = item.title } } }

装饰列表

本来,由于RecyclerView的 API,设置装饰与设置列表数据是分开的。如果您对装饰有一些逻辑,例如: all items should have offset at the bottom except the last one ,或者all items should have a divider at the bottom, but headers should have an offset at bottom那么创建装饰就会变得很痛苦。


通常团队都会配备可以配置的“上帝”装饰器:

 class SmartDividerItemDecorator( val context: Context, val skipDividerFor: Set<Int> = emptySet(), val showDividerAfterLastItem: Boolean = false, val showDividerBeforeFirstItem: Boolean = false, val dividerClipToPadding: Boolean = true, val dividerPaddingLeft: Int = 0, val dividerPaddingRight: Int = 0 ) : ItemDecoration()

您只能想象实现细节以及它是多么脆弱且不可扩展。


我们可以使用一种经过验证的旧方法,模仿某些RecyclerView API 并委托实现。


所以我从一个数据开始:

 interface Decoration


装饰的界面标记,将在未来呈现。

 interface HasDecorations { var decorations: List<Decoration> }

这个接口应该由我们的数据项来实现,以声明这个特定的项目想要用给定的装饰列表来装饰。为了简单起见,装饰是var ,如果需要的话,可以在运行时更改它。


通常一个项目只有一个装饰,因此为了通过将单个项目包装到listOf()来减少样板,我们可以执行以下操作:

 interface HasDecoration : HasDecorations { var decoration: Decoration override var decorations: List<Decoration> get() = listOf(decoration) set(value) { decoration = when { value.isEmpty() -> None value.size == 1 -> value.first() else -> throw IllegalArgumentException("Applying few decorations to HasDecoration instance is prohibited. Use HasDecorations") } } }


下一阶段是模仿ItemDecoration API:

 interface DecorationDrawer<T : Decoration> { fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: T ) fun onDraw( c: Canvas, view: View, parent: RecyclerView, state: RecyclerView.State, decoration: T ) }

正如您所看到的,它完全重复了ItemDecoration只有一个区别 - 现在它知道装饰类型和实例即将到来。


DecorationDrawer的可能实现是第二篇文章的主题,现在我们只关注接口以及如何处理它来呈现装饰。

 class CompositeItemDecoration( private val context: Context ) : RecyclerView.ItemDecoration() { private val drawers = mutableMapOf<Class<Decoration>, DecorationDrawer<Decoration>>() // [1] private fun applyDecorationToView(parent: RecyclerView, view: View, applyDecoration: (Decoration) -> Unit) { val position = getAdapterPositionForView(parent, view) // [2] when { // [3] position == RecyclerView.NO_POSITION -> return adapter.items[position] is HasDecorations -> { val decoration = (adapter.items[position] as HasDecorations).decorations decoration.forEach(applyDecoration) } else -> return } } override fun getItemOffsets( outRect: Rect, view: View, parent: RecyclerView, state: RecyclerView.State ) { applyDecorationToView(parent, view) { decoration -> val drawer = getDrawerFor(decoration) drawer.getItemOffsets(outRect, view, parent, state, decoration) } } override fun onDraw(c: Canvas, parent: RecyclerView, state: RecyclerView.State) { parent.forEach { child -> applyDecorationToView(parent, child) { decoration -> val drawer = getDrawerFor(decoration) drawer.onDraw(c, child, parent, state, decoration) } } } private fun getDrawerFor(decoration: Decoration): DecorationDrawer<Decoration> { // [4] } private fun getAdapterPositionForView(parent: RecyclerView, view: View): Int { var position = parent.getChildAdapterPosition(view) if (position == RecyclerView.NO_POSITION) { val oldPosition = parent.getChildViewHolder(view).oldPosition if (oldPosition in 0 until (parent.adapter?.itemCount ?: 0)) { position = oldPosition } } return position } }
  1. 缓存已创建的抽屉
  2. getAdapterPositionForView()不是特定于装饰应用的东西,它只是一种正确解析给定视图的适配器位置的方法
  3. 迭代HasDecorations实例的装饰
  4. 这件作品是第二篇文章“装饰点菜”的主题

结论

本文发现了一种围绕RecyclerView构建列表框架的方法。委托用作包装列表项和列表装饰的基本原则。


我认为建议的解决方案的主要优点是:

  1. 本土性。实现不依赖于第三方库,这可能会降低未来的灵活性。选择 AdapterDelegates 正是为了考虑这些标准 - 它非常简单、自然,所有行为都触手可及。

  2. 可扩展性。由于委托和关注点分离原则,我们能够解耦项目委托或装饰抽屉的实现。

  3. 可以轻松地增量集成到现有项目


在第二部分中,我们将讨论我们可能期望在 Android 应用程序中看到的所有类型的列表装饰。