一、总体目标与选型
-
多 ViewType:一个列表里混排 Banner、Card、Footer ...
-
局部刷新:只改动变化字段(避免整行重绑/闪烁)
-
稳定 ID :同一个"业务实体"在数据变化中身份不变,动画/复用更准
建议组合:ListAdapter + DiffUtil.ItemCallback + payload + 稳定 ID。
(已有自定义 Adapter 也可以按文末"手动 notify + payload"做。)
二、多 ViewType:三种常用写法
1)密钥型 viewType(最常见)
kotlin
enum class RowType(val code: Int) { BANNER(1), ARTICLE(2), FOOTER(3) }
sealed interface Row {
val id: Long
data class Banner(override val id: Long, val images: List<String>): Row
data class Article(override val id: Long, val title: String, val liked: Boolean): Row
data class Footer(override val id: Long, val text: String): Row
}
class FeedAdapter : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var items: List<Row> = emptyList()
override fun getItemViewType(position: Int): Int = when (items[position]) {
is Row.Banner -> RowType.BANNER.code
is Row.Article -> RowType.ARTICLE.code
is Row.Footer -> RowType.FOOTER.code
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
when (viewType) {
RowType.BANNER.code -> BannerVH(ItemBannerBinding.inflate(...))
RowType.ARTICLE.code -> ArticleVH(ItemArticleBinding.inflate(...))
RowType.FOOTER.code -> FooterVH(ItemFooterBinding.inflate(...))
else -> error("unknown viewType=$viewType")
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (holder) {
is BannerVH -> holder.bind(items[position] as Row.Banner)
is ArticleVH -> holder.bind(items[position] as Row.Article)
is FooterVH -> holder.bind(items[position] as Row.Footer)
}
}
override fun getItemCount() = items.size
}
要点
-
getItemViewType 返回稳定且有限的整型 code(不要用 position)。
-
每个类型一个 ViewHolder + bind(),onCreate 里做一次性的 setOnClickListener 等。
2)Adapter Delegates(业务拆分更清晰)
把每种类型的"是否能处理 + 创建 + 绑定"封装为 delegate,主 Adapter 只负责调度。利于多人协作,大项目推荐(库如 adapter-delegates)。
3)ConcatAdapter(多个适配器拼接)
头/正文/尾各自一个 Adapter,拼成一个列表;与稳定 ID、DiffUtil 也兼容(注意 StableIdMode,见文末)。
三、局部刷新(payload):两条路
路线 A:自己发
notifyItemChanged(position, payload)
当你明确知道第 N 行只改了"点赞数":
ini
adapter.notifyItemChanged(position, payload = LikeChanged(newLiked = true))
在 onBindViewHolder(holder, position, payloads) 里按 payload 只改对应控件:
kotlin
override fun onBindViewHolder(holder: RecyclerView.ViewHolder,
position: Int,
payloads: MutableList<Any>) {
if (payloads.isNotEmpty()) {
when (val p = payloads.last()) {
is LikeChanged -> (holder as ArticleVH).apply {
binding.likeIcon.isSelected = p.newLiked
binding.likeCount.text = p.newCount.toString()
}
// 其它 payload ...
}
return
}
super.onBindViewHolder(holder, position, payloads) // 回退到完整绑定
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
// 完整绑定:当 payloads 为空或首次展示时
when (holder) {
is ArticleVH -> holder.bind(items[position] as Row.Article)
// ...
}
}
路线 B:
DiffUtil.getChangePayload
自动算 payload(推荐)
配合 ListAdapter/AsyncListDiffer,让差分在后台算出变更字段 ,UI 线程只做增量绑定。
kotlin
class FeedAdapter :
ListAdapter<Row, RecyclerView.ViewHolder>(Diff()) {
override fun onCreateViewHolder(...): RecyclerView.ViewHolder { /* 同上 */ }
override fun onBindViewHolder(h: RecyclerView.ViewHolder, pos: Int) {
onBindViewHolder(h, pos, emptyList()) // 统一走带 payload 的重载
}
override fun onBindViewHolder(h: RecyclerView.ViewHolder, pos: Int, payloads: MutableList<Any>) {
val item = getItem(pos)
if (payloads.isNotEmpty()) {
when (val p = payloads.last()) {
is ArticlePayload -> (h as ArticleVH).apply {
p.title?.let { binding.title.text = it }
p.liked?.let { binding.likeIcon.isSelected = it }
p.likeCount?.let { binding.likeCount.text = it.toString() }
}
}
return
}
// 完整绑定
when (h) {
is ArticleVH -> h.bind(item as Row.Article)
// ...
}
}
class Diff : DiffUtil.ItemCallback<Row>() {
override fun areItemsTheSame(old: Row, new: Row) = old.id == new.id &&
old::class == new::class // 类型也要一致
override fun areContentsTheSame(old: Row, new: Row) = old == new
override fun getChangePayload(old: Row, new: Row): Any? = when {
old is Row.Article && new is Row.Article -> ArticlePayload(
title = new.title.takeIf { it != old.title },
liked = new.liked.takeIf { it != old.liked },
likeCount = /* compute new count if change */
null // 示例
)
else -> null
}
}
}
data class ArticlePayload(
val title: String? = null,
val liked: Boolean? = null,
val likeCount: Int? = null
)
好处:
- 不卡 UI:差分在后台线程;
- 更少闪烁:只更新变更控件;
- 动画友好:配合稳定 ID 效果最佳。
四、稳定 ID(Stable Ids):何时/如何用
为什么要开
-
让 RecyclerView 在数据变化时识别"它还是它",从而:
-
过渡动画更准确(新增/删除/移动/改变分得更清);
-
复用命中率更高,减少"整行重绑"的闪烁;
-
和 DefaultItemAnimator 的 change/move 配合更自然。
-
怎么开
kotlin
class FeedAdapter : ListAdapter<Row, RecyclerView.ViewHolder>(Diff()) {
init { setHasStableIds(true) }
override fun getItemId(position: Int): Long =
when (val item = getItem(position)) {
is Row.Banner -> (1L shl 60) or item.id // 不同类型占不同高位,避免冲突
is Row.Article -> (2L shl 60) or item.id
is Row.Footer -> (3L shl 60) or item.id
}
}
关键规则
-
ID 来源于业务唯一键 (如后端主键/UUID),永不随内容改变。
-
不要用 position 当 ID(滚动/插入就变了)。
-
多类型时确保不同类型不会 ID 撞车(上例用高位分桶是一招)。
-
先 setHasStableIds(true) 再 setAdapter(最佳实践)。
与 DiffUtil:两者并不冲突。areItemsTheSame 用相同的业务 ID;StableIds 让动画/复用更稳。
五、把三者串起来的模板(推荐直接用)
kotlin
class FeedAdapter : ListAdapter<Row, RecyclerView.ViewHolder>(Diff()) {
init { setHasStableIds(true) }
override fun getItemId(position: Int): Long = when (val it = getItem(position)) {
is Row.Banner -> (1L shl 60) or it.id
is Row.Article -> (2L shl 60) or it.id
is Row.Footer -> (3L shl 60) or it.id
}
override fun getItemViewType(position: Int): Int = when (getItem(position)) {
is Row.Banner -> RowType.BANNER.code
is Row.Article -> RowType.ARTICLE.code
is Row.Footer -> RowType.FOOTER.code
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder =
when (viewType) {
RowType.BANNER.code -> BannerVH(ItemBannerBinding.inflate(...))
RowType.ARTICLE.code -> ArticleVH(ItemArticleBinding.inflate(...))
RowType.FOOTER.code -> FooterVH(ItemFooterBinding.inflate(...))
else -> error("unknown viewType")
}
override fun onBindViewHolder(h: RecyclerView.ViewHolder, pos: Int) =
onBindViewHolder(h, pos, emptyList())
override fun onBindViewHolder(h: RecyclerView.ViewHolder, pos: Int, payloads: MutableList<Any>) {
val item = getItem(pos)
if (payloads.isNotEmpty()) {
when (val p = payloads.last()) {
is ArticlePayload -> (h as ArticleVH).bindPartial(p)
// 其它类型 payload ...
else -> Unit
}
return
}
when (h) {
is BannerVH -> h.bind(item as Row.Banner)
is ArticleVH -> h.bind(item as Row.Article)
is FooterVH -> h.bind(item as Row.Footer)
}
}
}
六、性能与避坑清单(和这三点强相关)
- 关闭不必要的 change 动画(防轻微变更也闪烁)
ini
(recyclerView.itemAnimator as? SimpleItemAnimator)
?.supportsChangeAnimations = false
- payload 必须"增量绑定" :别在 payload 分支里又重绑整行控件(等于白做)。
- areItemsTheSame 与稳定 ID 对齐:都用同一"业务 id"。
- getItemViewType 必须稳定:数据变更不能改变"同一条数据的类型编号",否则复用/动画会错乱。
- 图片/异步加载需可取消:否则快速滚动时因重用导致"错位图"。
- ConcatAdapter + 稳定 ID:在构造时配置
scss
ConcatAdapter(
ConcatAdapter.Config.Builder()
.setStableIdMode(ConcatAdapter.Config.StableIdMode.ISOLATED_STABLE_IDS) // 或 SHARED
.build(),
headerAdapter, bodyAdapter, footerAdapter
)
-
- ISOLATED_STABLE_IDS:子适配器 ID 自动隔离,不担心冲突。
- SHARED_STABLE_IDS:你保证全局唯一,换来更少开销。
一句话收束
多类型 决定"用哪个 VH 绑定谁";payload 局部刷新 只改动变更字段(手动或由 DiffUtil 生成);稳定 ID 让"它还是它"------复用与动画更稳更准。三者合用,列表既流畅 又不闪 ,代码也更工程化。