RecyclerView 进阶绑定:多类型 / 局部刷新(payload)/ 稳定 ID

一、总体目标与选型

  • 多 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 让"它还是它"------复用与动画更稳更准。三者合用,列表既流畅不闪 ,代码也更工程化

相关推荐
Hilaku4 小时前
为什么我开始减少逛技术社区,而是去读非技术的书?
前端·javascript·面试
南北是北北4 小时前
RecyclerView 的关键角色与各自职责/协同关系
面试
沐怡旸4 小时前
【底层机制】Handler/Looper 实现线程切换的技术细节
android·面试
自由的疯5 小时前
优雅的代码java
java·后端·面试
.NET修仙日记5 小时前
2025年ASP.NETMVC面试题库全解析
面试·职场和发展·c#·asp.net·mvc·面试题·asp.net mvc
绝无仅有5 小时前
面试真实经历某商银行大厂Java问题和答案总结(一)
后端·面试·github
绝无仅有5 小时前
面试真实经历某商银行大厂Java问题和答案总结(二)
后端·面试·github
召摇5 小时前
深入Next.js应用性能优化:懒加载技术全解析
前端·面试·next.js
召摇5 小时前
Next.js Server Actions进阶指南:安全传递额外参数的完整方案
前端·面试·next.js