如今,Jetpack Compose 获得了广泛的采用,而且它绝对值得。这是 UI 开发中真正的范式转变。那里消除了很多创建列表的样板。而且它很漂亮。 Android 我相信许多项目仍在使用 View 框架,因此使用 RecyclerView 来拥有列表。 据我看来,有以下几个原因: 一些围绕RecyclerView的内部专有解决方案与Compose不容易匹配,因此迁移成本很高 开发人员对 Compose 还不够熟悉 对 Compose 的性能没有信心(还) 但无论如何,Compose 是未来,这就是我在标题中使用“复古”一词的原因。 复古意味着古老但黄金。这篇文章是我对 RecyclerView 的使用经验的总结,对于那些仍在使用 RecyclerView 的人,我将讲述我围绕 RecyclerView 构建框架的愿景。 要求 该框架应该提供合理的可扩展性。因此,主要场景应该用最少量的样板来处理,但如果用户想要创建自定义的东西,它不应该成为障碍。 我将在使用 RecyclerView 期间定义基本场景集,如下所示: 有效更新数据。 众所周知,使用 RecyclerView 期间的最佳实践是使用 API 来更新列表中的数据。我们不打算在这里重新发明轮子,因此我们将专注于利用 ,并尝试减少其实施的仪式。 DiffUtil DiffUtil 定义异构列表。 列表中自然会有多种类型的视图。移动设备的屏幕受到高度的限制,不同的设备具有不同的屏幕尺寸。因此,即使您的内容在一台设备上适合屏幕而不滚动,也可能需要在另一台设备上滚动。 这就是为什么在 RecyclerView 顶部开发一些屏幕是个好主意。 装饰列表项 装饰是列表中非常常见的情况。尽管您可以将装饰合并到项目布局中,但看起来很简单,但它不是很灵活:相同的项目类型可能需要在不同屏幕上使用不同的装饰,甚至根据列表中项目的位置需要不同的装饰。 基础选项 第一个选择是一切从头开始。它提供了对实施的完全控制,但我们始终可以做到。 有许多第三方开源框架允许使用更少的样板文件来使用 RecyclerView。我不会一一列举,但想用完全相反的方法展示其中两个: 环氧树脂 AirBnb 的解决方案,用于使用 RecyclerView 创建复杂的屏幕。它添加了一堆新的 API,甚至还添加了代码生成,以尽可能减少样板文件。 但我无法摆脱这样的感觉:生成的代码看起来很陌生。此外,注释处理又增加了一层复杂性。 适配器代表 微小的库,实际上提供了一组简洁的函数和接口,仅建议您将异构列表拆分为一组委托适配器的方法。事实上,您可以在相对较短的时间内创建自己的解决方案。 我喜欢小型的本机解决方案,它们足够简单,可以让所有行为触手可及,并且不会隐藏所有实现细节。这就是为什么我相信 AdapterDelagates 及其原则是我们框架基础的良好候选者。 列出项目 最开始的最基本的事情是列表项的通用声明。现实世界中的列表项有很大不同,但我们唯一有信心的是——我们应该能够对其进行比较。 我建议参考 的 API DiffUtil.ItemCallback interface ListItem { fun isItemTheSame(other: ListItem): Boolean fun isContentTheSame(other: ListItem): Boolean { return this == other } } 这是框架中列表项的简约(因此是可靠的,IMO)声明。方法的语义与 DiffUtil 相同,使用: 检查 和 的身份 isItemTheSame other this 检查 和 中的数据 isContentTheSame other this 提供稳定且唯一身份的最常见方法是使用来自服务器的标识符。 因此,让我们有一个抽象实现来减少一些可能的样板代码: 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 } } } 装饰列表 本来,由于 的 API,设置装饰与设置列表数据是分开的。如果您对装饰有一些逻辑,例如: ,或者 那么创建装饰就会变得很痛苦。 RecyclerView 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() 您只能想象实现细节以及它是多么脆弱且不可扩展。 我们可以使用一种经过验证的旧方法,模仿某些 API 并委托实现。 RecyclerView 所以我从一个数据开始: 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") } } } 下一阶段是模仿 API: ItemDecoration 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 } } 缓存已创建的抽屉 不是特定于装饰应用的东西,它只是一种正确解析给定视图的适配器位置的方法 getAdapterPositionForView() 迭代 实例的装饰 HasDecorations 这件作品是第二篇文章“装饰点菜”的主题 结论 本文发现了一种围绕 构建列表框架的方法。委托用作包装列表项和列表装饰的基本原则。 RecyclerView 我认为建议的解决方案的主要优点是: 本土性。实现不依赖于第三方库,这可能会降低未来的灵活性。选择 AdapterDelegates 正是为了考虑这些标准 - 它非常简单、自然,所有行为都触手可及。 可扩展性。由于委托和关注点分离原则,我们能够解耦项目委托或装饰抽屉的实现。 可以轻松地增量集成到现有项目 在第二部分中,我们将讨论我们可能期望在 Android 应用程序中看到的所有类型的列表装饰。