第6周:RecyclerView 真正难的不是“写个列表”,而是让列表在复用中保持正确

前 5 周我们一直在处理单个控件和资源治理。到了第 6 周,页面开始进入真实业务最常见的形态:列表。

列表不是把一堆 View 竖着摆出来。真实 App 里的列表会不断滚动、刷新、插入、删除、切换状态,还要承受图片、点击、曝光、埋点和分页。RecyclerView 的价值就在这里:它把"容器、排列、复用、数据绑定"拆开,让列表可以在大量数据下仍然保持可控。

本周 Demo 已落到当前项目:Week6RecyclerViewActivity。它不是一个只显示文字的最小示例,而是同时做了:

  • 垂直 / 横向列表切换
  • 横向场景列表
  • 单布局 / 多布局
  • 点击事件和 NO_POSITION 防御
  • ItemDecoration 间距
  • notifyItemChanged(position, payload) 局部刷新
  • setHasFixedSizesetItemViewCacheSizeRecycledViewPool 基础复用配置
  • 完整绑定时清理复用残留状态

相关资料

资料 本文使用点
Android Developers:使用 RecyclerView 创建动态列表 确认 RecyclerViewAdapterViewHolderLayoutManager 的官方职责拆分,以及 View 复用机制
Android Developers:RecyclerView API Reference 查证 setHasFixedSizeRecycledViewPoolItemDecoration、position 相关边界
Android Developers:DiffUtil API Reference 查证 DiffUtil 用途、列表不可原地修改、大列表建议后台计算、算法复杂度边界
Android Developers:ListAdapter API Reference 查证 ListAdapterAsyncListDiffer 的便捷封装,使用 submitList() 提交新列表

第 6 周按规划主线做 RecyclerView 基础功能和基础优化。DiffUtil / ListAdapter 本文会讲清楚它们是什么、为什么需要、边界在哪里;第 7 周会进入高级列表优化,再深入它们的完整使用。

一、RecyclerView 到底替你解决了什么

RecyclerView 是 AndroidX 里专门展示动态列表和网格的组件。它自己是一个 ViewGroup,但它不直接知道"你的商品长什么样""你的会话怎么绑定""你的标题行和内容行怎么区分"。这些事情被拆给了三类角色:

  • LayoutManager:决定 item 怎么排列,是竖向、横向、网格还是瀑布流。
  • Adapter:负责把数据变成一个个可展示的 item。
  • ViewHolder:持有单个 item 的 View,避免反复查找子控件。

Demo 的页面里先放了两个 RecyclerView:一个横向场景列表,一个主列表。

ini 复制代码
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rvScenes"
    android:layout_width="match_parent"
    android:layout_height="132dp"
    android:layout_marginTop="8dp"
    android:clipToPadding="false"
    android:overScrollMode="never" />
​
<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/rvMain"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_marginTop="8dp"
    android:layout_weight="1"
    android:clipToPadding="false"
    android:overScrollMode="never"
    android:paddingBottom="12dp" />

这段代码做了三件事:

  1. rvScenes 用固定高度展示横向列表,适合模拟首页频道、商品推荐、标签流。
  2. rvMainlayout_height="0dp" + layout_weight="1" 占据剩余空间,避免把主列表塞进 ScrollView 里。
  3. clipToPadding="false" 让列表底部 padding 不裁掉滚动内容,滚动到底时视觉更自然。

少了 layout_weight="1",主列表就不能稳定占据剩余空间;如果把主列表直接放进外层 ScrollView 并让它 wrap_content,就会让 RecyclerView 的复用优势被削弱,甚至出现测量成本过高的问题。

成熟团队做列表页时,一般不会把所有内容都塞进一个大滚动容器里"凑效果"。电商商品流、内容 Feed、IM 会话列表这类页面,主滚动容器通常只有一个,其他头部、筛选、横向推荐位再通过明确的布局关系或多类型 item 组合进去。

相关技术清单:RecyclerViewViewGroupLayoutManagerLinearLayoutManagerclipToPadding、主滚动容器治理。

二、LayoutManager:列表方向不应该写死在 Adapter 里

官方文档把 LayoutManager 定义为排列 item 的角色。也就是说,Adapter 只负责"有什么数据、用什么 item 绑定",不负责"竖着排还是横着排"。

Demo 里两个列表用的是同一类 Adapter,但方向由 LinearLayoutManager 控制:

ini 复制代码
binding.rvScenes.layoutManager = LinearLayoutManager(this, RecyclerView.HORIZONTAL, false)
binding.rvScenes.adapter = sceneAdapter
​
binding.rvMain.layoutManager = LinearLayoutManager(this, RecyclerView.VERTICAL, false)
binding.rvMain.adapter = mainAdapter

这段代码里,RecyclerView.HORIZONTALRecyclerView.VERTICAL 决定排列方向。Adapter 不需要知道自己被用在横向列表还是垂直列表里。

Demo 还做了一个"切换方向"按钮:

ini 复制代码
binding.btnToggleOrientation.setOnClickListener {
    mainListIsHorizontal = !mainListIsHorizontal
    val orientation = if (mainListIsHorizontal) RecyclerView.HORIZONTAL else RecyclerView.VERTICAL
    binding.rvMain.layoutManager = LinearLayoutManager(this, orientation, false)
}

少了 layoutManagerRecyclerView 不知道怎么摆放 item,页面不会正常显示列表。把方向判断写进 Adapter,则会让 Adapter 同时关心"数据绑定"和"布局排列",后续做网格、横向、瀑布流时很难维护。

成熟团队常见做法是:业务 Adapter 尽量保持稳定,列表形态由页面层或列表容器决定。比如搜索结果页可能从单列切到双列,频道页可能有横向推荐位,但数据绑定逻辑不应该因此散落在多个 Adapter 里重复实现。

相关技术清单:LinearLayoutManagerRecyclerView.VERTICALRecyclerView.HORIZONTAL、职责分离。

三、Adapter / ViewHolder:一个管数据入口,一个管 item 视图

Adapter 是 RecyclerView 和业务数据之间的桥。它至少回答三个问题:

  1. 有多少个 item:getItemCount()
  2. 创建什么样的 item View:onCreateViewHolder()
  3. 某个位置的数据怎么绑定到 View:onBindViewHolder()

本周 Demo 的数据先用一个 sealed class 表达:

kotlin 复制代码
private sealed class Week6ListItem {
    abstract val id: Long
​
    data class Section(
        override val id: Long,
        val title: String,
        val summary: String
    ) : Week6ListItem()
​
    data class Card(
        override val id: Long,
        val title: String,
        val description: String,
        val tag: String,
        val clickCount: Int = 0,
        val selected: Boolean = false
    ) : Week6ListItem()
}

这里没有直接用 String 列表,是因为真实列表通常不是纯文本。一个页面里可能有分组标题、商品卡、广告卡、推荐卡、空状态卡。先把数据类型拆清楚,后面的多布局才不会混乱。

少了 id,短期也能跑,但后续接 DiffUtil 时就缺少稳定判断"是不是同一个业务实体"的基础。少了 selectedclickCount 这类状态字段,点击后的 UI 状态就只能散落在 View 上,复用时更容易错乱。

ViewHolder 负责持有 item 布局,并把数据写进具体控件:

kotlin 复制代码
private inner class CardHolder(
    private val binding: ItemWeek6ListCardBinding,
    private val onItemClick: (position: Int, item: Week6ListItem) -> Unit
) : RecyclerView.ViewHolder(binding.root) {
​
    fun bind(item: Week6ListItem.Card) {
        binding.tvItemTitle.text = item.title
        binding.tvItemDescription.text = item.description
        binding.tvItemTag.text = item.tag
        bindSelection(item.selected, item.clickCount)
    }
}

这段代码的重点是:完整绑定时,不只设置标题和描述,也要设置选中态。因为 ViewHolder 会复用,旧 item 留下来的背景、描边、点击次数都可能污染新 item。

少了 bindSelection(item.selected, item.clickCount),你会看到很典型的 RecyclerView 复用 bug:刚才点过的卡片滚出屏幕后,再滚回来,另一个位置可能显示成"已选中"。

成熟团队做 IM 会话、金融账单、商品卡片时,都会要求完整 bind 能还原 item 的全部 UI 状态。不要把"默认未选中""默认灰色""默认隐藏"交给 XML 初始值,因为 View 复用后 XML 初始值不会自动重来一次。

相关技术清单:RecyclerView.AdapterRecyclerView.ViewHolderViewBinding、完整绑定、复用状态清理。

四、多布局:getItemViewType() 不是炫技,是让列表结构可维护

本周主列表里同时有分组标题和业务卡片。它们不是同一种 View,所以 Adapter 要告诉 RecyclerView:不同位置应该创建不同 ViewHolder。

kotlin 复制代码
override fun getItemViewType(position: Int): Int = when (items[position]) {
    is Week6ListItem.Section -> VIEW_TYPE_HEADER
    is Week6ListItem.Card -> VIEW_TYPE_CARD
}
​
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    val inflater = LayoutInflater.from(parent.context)
    return when (viewType) {
        VIEW_TYPE_HEADER -> HeaderHolder(
            ItemWeek6SectionHeaderBinding.inflate(inflater, parent, false)
        )
        VIEW_TYPE_CARD -> CardHolder(
            ItemWeek6ListCardBinding.inflate(inflater, parent, false),
            onItemClick
        )
        else -> error("Unknown viewType: $viewType")
    }
}

getItemViewType() 返回的是 View 类型,不是业务分类名。RecyclerView 会按 viewType 管理回收池,所以同一个 viewType 必须创建兼容的 ViewHolder。

少了 getItemViewType(),RecyclerView 默认所有 item 都是同一种类型。你要么只能把标题和卡片硬塞进同一个布局里,要么在 onBindViewHolder() 里写大量 if 去隐藏控件,后面维护会很痛苦。

这里还有一个容易踩的坑:多个 RecyclerView 共享 RecycledViewPool 时,viewType 的语义必须兼容。Demo 中横向列表和主列表都使用同一个 Week6AdapterVIEW_TYPE_CARD 对应的布局和 ViewHolder 是同一套,所以可以共享。真实项目里如果两个 Adapter 都把 1 当 viewType,但一个代表商品卡、一个代表广告卡,就不能随便共享回收池。

成熟团队在复杂首页、内容社区 Feed、搜索聚合页里,常会把多类型 item 做成明确的 item model,而不是让一个巨大布局靠隐藏显示来适配所有场景。这样做的代价是 Adapter 结构更复杂,但收益是复用、局部刷新和调试都更清楚。

相关技术清单:getItemViewType()、多布局 Adapter、viewType 兼容性、RecycledViewPool 共享边界。

五、点击事件:不要把 position 当成永远正确

列表点击最容易写成这样:创建 ViewHolder 时把 position 存下来,点击时直接用。这个写法在 RecyclerView 里不稳,因为列表可能插入、删除、刷新,ViewHolder 当前对应的位置会变。

Demo 里点击时才读取 bindingAdapterPosition

arduino 复制代码
init {
    binding.root.setOnClickListener {
        val position = bindingAdapterPosition
        if (position == RecyclerView.NO_POSITION) return@setOnClickListener
        onItemClick(position, items[position])
    }
}

这段代码做了两个防御:

  1. 点击发生时才拿当前位置,而不是提前缓存 position。
  2. 遇到 RecyclerView.NO_POSITION 直接返回。

NO_POSITION 代表当前 ViewHolder 没有有效 Adapter 位置。比如 Adapter 刚更新、布局还没稳定时,就可能出现这种临界状态。

少了 NO_POSITION 判断,轻则点击错数据,重则数组越界。少了点击时再取 bindingAdapterPosition,就可能把旧位置的数据当成当前 item。

成熟团队做列表点击时,通常还会把点击事件上抛给页面层,由页面层决定跳转、埋点、弹窗或局部更新。Adapter 可以承载基础交互,但不应该变成业务流程中心。

相关技术清单:bindingAdapterPositionNO_POSITION、点击事件上抛、Adapter 位置和布局位置差异。

六、ItemDecoration:分割线和间距不一定要写进 item XML

ItemDecoration 是 RecyclerView 提供的装饰机制。官方定义里它可以给特定 item 添加绘制和布局偏移,常见用途就是分割线、间距、分组装饰。

Demo 写了一个只负责间距的 Decoration:

kotlin 复制代码
private class Week6SpacingDecoration(
    private val spacePx: Int,
    private val horizontal: Boolean = false
) : RecyclerView.ItemDecoration() {
​
    override fun getItemOffsets(
        outRect: Rect,
        view: View,
        parent: RecyclerView,
        state: RecyclerView.State
    ) {
        val position = parent.getChildAdapterPosition(view)
        if (position == RecyclerView.NO_POSITION) return
​
        if (horizontal) {
            if (position == 0) outRect.left = spacePx
            outRect.right = spacePx
        } else {
            outRect.bottom = spacePx
        }
    }
}

这段代码只改变 item 周围的 offset,不改变 Adapter 数据,也不改变 item 的 position。它让"列表间距规则"从 item XML 中独立出来。

少了 getChildAdapterPosition(view) 的有效性判断,列表更新或动画期间可能拿到无效位置。少了 horizontal 分支,横向列表和竖向列表就会共用一套间距逻辑,视觉上很容易不对。

什么时候适合用 ItemDecoration

  • 统一分割线
  • 统一 item 间距
  • 分组背景
  • 吸顶标题背景
  • 不想让每个 item 自己关心"我和别人之间隔多远"

什么时候不适合?如果某个间距是 item 内容的一部分,比如卡片内部 padding、头像和标题之间的距离,就应该留在 item XML 里。

成熟团队做列表视觉治理时,会尽量把"列表规则"和"item 内容结构"分开。这样同一个商品卡可以放在搜索页、推荐页、店铺页,而不同页面的外部间距由 RecyclerView 层处理。

相关技术清单:RecyclerView.ItemDecorationgetItemOffsets()Rect、分割线、item 间距治理。

七、局部刷新:notifyItemChanged(position, payload) 不是自动魔法

列表里最常见的更新不是整条数据变了,而是某个字段变了:点赞数、选中态、未读数、进度条、关注状态。

本周 Demo 点击卡片后,只更新选中态和点击次数:

kotlin 复制代码
fun toggleSelection(position: Int) {
    val current = items.getOrNull(position) as? Week6ListItem.Card ?: return
    val updated = current.copy(
        selected = !current.selected,
        clickCount = current.clickCount + 1
    )
    items[position] = updated
    notifyItemChanged(
        position,
        Week6Payload.SelectionChanged(updated.selected, updated.clickCount)
    )
}

这段代码先更新数据,再通知 RecyclerView:这个位置的 item 内容发生了变化,并且变化内容是 SelectionChanged

少了 items[position] = updated,界面也许会临时变,但下一次完整 bind 又会回到旧数据。少了 payload,Adapter 只能走普通的完整绑定,不知道这次只需要改选中态。

Adapter 里还要重写带 payload 的 onBindViewHolder()

kotlin 复制代码
override fun onBindViewHolder(
    holder: RecyclerView.ViewHolder,
    position: Int,
    payloads: MutableList<Any>
) {
    if (payloads.isEmpty() || holder !is CardHolder) {
        onBindViewHolder(holder, position)
        return
    }
​
    payloads.filterIsInstance<Week6Payload.SelectionChanged>()
        .lastOrNull()
        ?.let { holder.bindSelection(it.selected, it.clickCount) }
        ?: onBindViewHolder(holder, position)
}

这里有一个关键边界:payload 只是"变化提示",不是自动更新 UI。你传了 payload,也必须在 Adapter 里解释它。

少了 payloads.isEmpty() 的完整绑定兜底,会出问题。因为官方 API 的语义里,payload 并不是强保证;如果目标 item 当前不在屏幕上,payload 可能不会按你想象的方式传到 ViewHolder。等它重新显示时,完整 bind 必须能恢复全部 UI。

成熟团队在 IM 会话列表、电商商品卡、内容 Feed 点赞状态里,会优先用局部刷新降低无效绑定。但不会为了"局部刷新"牺牲完整绑定可靠性。完整 bind 是底线,payload 是优化。

相关技术清单:notifyItemChanged(position)notifyItemChanged(position, payload)、payload、完整 bind、局部刷新、UI 状态一致性。

八、复用和缓存:优化前先知道自己在优化什么

RecyclerView 的性能基础来自复用:滚出屏幕的 View 不会马上销毁,而是被拿来绑定新的数据。这个机制减少了创建 View 和 inflate 布局的成本。

Demo 做了三类基础配置:

scss 复制代码
sharedPool.setMaxRecycledViews(Week6Adapter.VIEW_TYPE_HEADER, 4)
sharedPool.setMaxRecycledViews(Week6Adapter.VIEW_TYPE_CARD, 16)
​
binding.rvMain.setHasFixedSize(true)
binding.rvMain.setItemViewCacheSize(4)
binding.rvMain.setRecycledViewPool(sharedPool)

这几行很容易被误解,所以要拆开讲。

setHasFixedSize(true) 的意思是:Adapter 内容变化不会影响 RecyclerView 自身尺寸。它不是说每个 item 高度都必须固定,也不是说数据不能变化。Demo 里主列表高度由父布局和 layout_weight 决定,所以适合打开。

setItemViewCacheSize(4) 是让单个 RecyclerView 在 View 进入共享回收池之前,额外保留一定数量的离屏 View。它不是越大越好,过大反而会占内存。

RecycledViewPool 保存的是可复用的 ViewHolder,不是业务数据。多个 RecyclerView 共享它的前提是 viewType 和 ViewHolder 兼容。本周横向列表和主列表都使用同一个 Adapter,所以共享是安全的。

少了 setRecycledViewPool(sharedPool),两个列表就不会共享这套回收池。少了 setMaxRecycledViews(),仍然可以复用,但你无法针对具体 viewType 调整池大小。滥用 setHasFixedSize(true) 则可能在 wrap_content 列表里造成布局更新不符合预期。

成熟团队不会把这些 API 当"性能玄学开关"。它们通常会结合场景使用:

  • 首页多个横向频道列表:考虑共享兼容的 RecycledViewPool
  • 固定高度的全屏列表:可以考虑 setHasFixedSize(true)
  • 高成本 item:评估缓存数量,但要监控内存。
  • item 状态复杂:优先保证完整 bind 清理状态,再谈缓存。

相关技术清单:View 复用、RecycledViewPoolsetHasFixedSizesetItemViewCacheSize、viewType 兼容、内存边界。

九、DiffUtil / ListAdapter

DiffUtil 是用来比较旧列表和新列表差异的工具。它会计算插入、删除、移动、内容变化,再把结果分发给 Adapter。它解决的是:不要一有变化就 notifyDataSetChanged() 整屏刷新。

ListAdapterRecyclerView.Adapter 的一个基类,内部封装了 AsyncListDiffer,可以在后台线程计算列表差异。它通过 submitList() 提交新列表,而不是让你直接修改内部 list。

典型形态长这样:

kotlin 复制代码
object ItemDiff : DiffUtil.ItemCallback<Item>() {
    override fun areItemsTheSame(oldItem: Item, newItem: Item): Boolean {
        return oldItem.id == newItem.id
    }
​
    override fun areContentsTheSame(oldItem: Item, newItem: Item): Boolean {
        return oldItem == newItem
    }
}

少了 areItemsTheSame(),DiffUtil 不知道两个 item 是否代表同一个业务实体。少了 areContentsTheSame(),它不知道同一个实体内容有没有变。判断写得太粗,会漏刷新;判断写得太细或永远返回 false,会产生大量不必要动画。

边界必须记住:

  • 不要原地修改旧 list 或 getCurrentList()
  • 数据变化时提交新 list。
  • 大列表 diff 计算可能有成本,官方建议使用后台计算能力。
  • 比较逻辑不能太重。
  • DiffUtil 不是数据库、不是分页、不是状态管理,它只负责列表差异计算。

成熟团队在搜索结果、商品流、会话列表里,通常会用 Diff 思路减少整屏刷新。但这类能力要和数据不可变、状态建模、分页加载一起设计,不是把 Adapter 换成 ListAdapter 就自动高级。

相关技术清单:DiffUtilDiffUtil.ItemCallbackListAdapterAsyncListDiffersubmitList()、不可变列表、差异刷新。

十、列表和图片加载协同

第 4 周和图片加载专题已经讲过 Glide、Coil、Fresco 的基本边界。到了 RecyclerView,图片问题会更明显:ViewHolder 会复用,旧图片请求可能晚回来,图片尺寸可能不匹配,动图可能让滚动掉帧。

这周只记三条实践边界:

  1. 完整 bind 里必须给图片 View 设置当前 item 的图片请求,不能依赖旧状态。
  2. 如果当前位置不需要图片,要清理或设置明确占位,避免复用错位。
  3. 大量图片、GIF、视频封面列表要结合图片框架生命周期和滚动状态做更细控制。

相关技术清单:图片复用错位、占位图、请求清理、滚动状态、图片预加载边界。

十一、第6周技术清单

技术 它是什么 Demo 落点 真实项目价值 常见坑
RecyclerView AndroidX 动态列表容器 activity_week6_recycler_view.xmlrvMain / rvScenes 支撑商品流、Feed、会话列表、搜索结果 放进外层大 ScrollView 导致复用优势变弱
Adapter 数据和 item View 的桥 Week6Adapter 集中管理 item 创建、绑定和数量 把跳转、网络请求、业务流程全塞进 Adapter
ViewHolder 单个 item View 的持有者 HeaderHolder / CardHolder 减少重复查找 View,承载绑定逻辑 只改部分字段,复用后残留旧状态
LayoutManager 控制 item 排列方式 LinearLayoutManager 横向 / 纵向切换 同一 Adapter 可适配不同列表形态 把横竖方向写进 Adapter
getItemViewType() 告诉 RecyclerView 当前位置是什么 View 类型 Section / Card 两种类型 支撑复杂首页、Feed 多类型卡片 viewType 语义冲突,导致回收错乱
ItemDecoration 给 item 添加绘制或布局偏移 Week6SpacingDecoration 统一治理分割线、间距、分组背景 把 item 内部 padding 和列表外部间距混在一起
notifyItemChanged 通知某个 item 内容变化 toggleSelection() 避免整屏刷新,提升状态更新效率 用它处理插入、删除、移动这类结构变化
payload 局部变化提示对象 Week6Payload.SelectionChanged 点赞、选中、未读数等局部更新更轻 以为 payload 会自动更新 UI,忘记完整 bind 兜底
bindingAdapterPosition ViewHolder 当前绑定 Adapter 中的位置 点击事件里实时读取 避免点击旧位置导致错数据 创建 ViewHolder 时缓存 position
NO_POSITION 当前没有有效 Adapter 位置 点击防御判断 防止更新临界状态下数组越界 不判断直接访问 items[position]
setHasFixedSize 声明 RecyclerView 自身尺寸不受 Adapter 内容影响 rvMain.setHasFixedSize(true) 减少不必要测量布局 误以为 item 高度必须固定,或在 wrap_content 场景滥用
setItemViewCacheSize 设置离屏 View 缓存数量 rvMain.setItemViewCacheSize(4) 降低高频返回 item 的重新绑定成本 越设越大导致内存压力
RecycledViewPool 可共享的 ViewHolder 回收池 sharedPool 同时给两个列表使用 多个兼容列表复用 ViewHolder 不同 Adapter 的 viewType 不兼容仍强行共享
DiffUtil 比较新旧列表差异的工具 本周只做边界说明 精确分发插入、删除、移动、修改 原地修改 list,比较逻辑过重或错误
ListAdapter 封装 AsyncListDiffer 的 Adapter 基类 本周只做边界说明 后台 diff + submitList() 简化列表更新 直接修改 getCurrentList() 返回值
ViewBinding 根据 XML 生成类型安全绑定类 ActivityWeek6RecyclerViewBinding 避免 findViewById 和类型转换错误 忘记布局 id 会导致 binding 字段不存在

十二、本周真正要记住什么

第 6 周不是为了背 RecyclerView API,而是建立列表开发的底层判断:

  • 列表方向交给 LayoutManager
  • 数据入口交给 Adapter
  • item 视图交给 ViewHolder
  • 多布局用 getItemViewType() 明确区分。
  • 点击时实时拿 bindingAdapterPosition,并处理 NO_POSITION
  • 间距和分割线优先考虑 ItemDecoration
  • 局部变化用 notifyItemChanged(position, payload),但完整 bind 必须可靠。
  • 缓存配置不是玄学,要知道 setHasFixedSizesetItemViewCacheSizeRecycledViewPool 分别优化什么。
  • DiffUtil / ListAdapter 是下一步,但不要在基础没打牢时直接跳模板。

下一周进入 RecyclerView 高级功能和硬核优化:下拉刷新、上拉加载、嵌套滑动、DiffUtil / ListAdapter 完整实践、滑动性能、预加载和过度绘制治理。

相关推荐
晓梦林2 小时前
EVA靶场学习笔记
android·笔记·学习
私人珍藏库2 小时前
【Android】抖音无水印下载安卓端 轻载 QingZai v1.0.4
android·app·工具·软件·多功能
qq3621967053 小时前
Twitter官网下载安装指南:2026最新安卓版APK教程
android·twitter
盼小辉丶3 小时前
PyTorch深度学习实战(55)——在Android上部署PyTorch模型
android·pytorch·python·模型部署
ImTryCatchException3 小时前
Android 卡顿诊断实战:从“感觉卡“到“精准定位“的方法论
android
vensli3 小时前
Wolverine:杀不死的 Android 进程保活方案
android
Meteors.13 小时前
安卓源码阅读——01.grade设置binding为true时,xml如何进行映射
android·xml
_李小白14 小时前
【android opencv学习笔记】Day 26: 滤波算法之低通滤波与图像缩放插值
android·opencv·学习
NiceCloud喜云15 小时前
Claude Code Routines 实战:三种触发器跑通云端自动化编码
android·运维·数据库·人工智能·自动化·json·飞书