如今,Jetpack Compose 获得了广泛的采用,而且它绝对值得。这是Android UI 开发中真正的范式转变。那里消除了很多创建列表的样板。而且它很漂亮。
我相信许多项目仍在使用 View 框架,因此使用 RecyclerView 来拥有列表。
但无论如何,Compose 是未来,这就是我在标题中使用“复古”一词的原因。
复古意味着古老但黄金。这篇文章是我对 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
检查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 } } }
本来,由于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 } }
getAdapterPositionForView()
不是特定于装饰应用的东西,它只是一种正确解析给定视图的适配器位置的方法HasDecorations
实例的装饰本文发现了一种围绕RecyclerView
构建列表框架的方法。委托用作包装列表项和列表装饰的基本原则。
本土性。实现不依赖于第三方库,这可能会降低未来的灵活性。选择 AdapterDelegates 正是为了考虑这些标准 - 它非常简单、自然,所有行为都触手可及。
可扩展性。由于委托和关注点分离原则,我们能够解耦项目委托或装饰抽屉的实现。
可以轻松地增量集成到现有项目
在第二部分中,我们将讨论我们可能期望在 Android 应用程序中看到的所有类型的列表装饰。