optimizing-lazy-layouts

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Optimizing Lazy Layouts — Keys, contentType, and animateItem()

优化惰性布局 — Keys、contentType与animateItem()

Lazy layouts compose only what's visible, but two things still cost: re-composition of items that should have been reused (missing
key
), and per-item allocation that compounds with scroll velocity (missing
contentType
, modifier chains created inside
items { }
). Both have a one-line fix. This skill teaches Claude how to apply that fix correctly and to validate that item composables are themselves skippable. Prefetch tuning is a separate concern — see
../configuring-lazy-prefetch/SKILL.md
.
惰性布局仅组合可见内容,但仍存在两类性能开销:本应复用的条目发生重组(缺少
key
),以及随滚动速度累积的每条目内存分配(缺少
contentType
、在
items { }
内部创建修饰符链)。这两类问题都只需一行代码即可修复。本技能将指导Claude如何正确应用修复方案,并验证条目可组合项是否可跳过重组。预取调优是独立的优化方向 — 请查看
../configuring-lazy-prefetch/SKILL.md

When to use this skill

何时使用此技能

  • The developer reports scroll jank, dropped frames, or stutter on a
    LazyColumn
    ,
    LazyRow
    ,
    LazyVerticalGrid
    , or
    LazyHorizontalGrid
    .
  • Items lose scroll position, focus, or composition state on insert, remove, or reorder.
  • A mixed-type feed (cards + headers + ads + carousels) feels sluggish even though each individual row is lightweight.
  • Modifier.animateItem()
    was added but no animation runs on inserts or removals.
  • The compiler report shows item composables as
    unstable
    /non-skippable, or
    @TraceRecomposition
    shows item composables recomposing on every scroll tick.
  • 开发者反馈
    LazyColumn
    LazyRow
    LazyVerticalGrid
    LazyHorizontalGrid
    存在滚动卡顿、丢帧或抖动问题。
  • 插入、删除或重排条目时,条目丢失滚动位置、焦点或组合状态。
  • 混合类型信息流(卡片 + 标题 + 广告 + 轮播图)即使单个条目轻量化,整体运行仍迟缓。
  • 添加了
    Modifier.animateItem()
    但插入或删除时无动画效果。
  • 编译器报告显示条目可组合项为
    unstable
    /不可跳过,或
    @TraceRecomposition
    显示条目可组合项在每次滚动周期都发生重组。

When NOT to use this skill

何时不使用此技能

  • The bottleneck is the prefetch window (heavy items, high-velocity scroll, want a wider ahead/behind window) → use
    ../configuring-lazy-prefetch/SKILL.md
    .
  • The item composable itself takes an unstable parameter (
    List<Foo>
    ,
    Flow<Foo>
    , a domain
    var
    ) → first run
    ../../stability/diagnosing-compose-stability/SKILL.md
    and then
    ../../stability/stabilizing-compose-types/SKILL.md
    .
  • An animation inside an item reads
    state.value
    in Composition phase, recomposing the row every frame → use
    ../../recomposition/deferring-state-reads/SKILL.md
    .
  • Scroll position derivation (e.g.
    firstVisibleItemIndex == 0
    ) is the hot path → use
    ../../recomposition/choosing-derivedstateof/SKILL.md
    .
  • 性能瓶颈在于预取窗口(条目过重、高速滚动、需要扩大前后预取范围)→ 使用
    ../configuring-lazy-prefetch/SKILL.md
  • 条目可组合项本身接收不稳定参数(
    List<Foo>
    Flow<Foo>
    、可变域对象
    var
    )→ 先运行
    ../../stability/diagnosing-compose-stability/SKILL.md
    ,再使用
    ../../stability/stabilizing-compose-types/SKILL.md
  • 条目内部的动画在组合阶段读取
    state.value
    ,导致每行在每一帧都发生重组 → 使用
    ../../recomposition/deferring-state-reads/SKILL.md
  • 滚动位置推导(如
    firstVisibleItemIndex == 0
    )是性能热点 → 使用
    ../../recomposition/choosing-derivedstateof/SKILL.md

Prerequisites

前置条件

  • Compose Foundation 1.7+ for
    Modifier.animateItem()
    (the GA replacement for the experimental
    animateItemPlacement
    ).
  • Kotlin 2.0.0+ with
    org.jetbrains.kotlin.plugin.compose
    applied. Strong Skipping is on by default; non-skippable item composables become amplified at scroll speed.
  • A real device + release build for measurement. Skydoves hot take #5: debug builds lie (Live Literals, interpreted mode). See
    ../../measurement/generating-baseline-profiles/SKILL.md
    when ready to measure.
  • 用于
    Modifier.animateItem()
    的Compose Foundation 1.7+(实验性
    animateItemPlacement
    的正式替代方案)。
  • 应用了
    org.jetbrains.kotlin.plugin.compose
    的Kotlin 2.0.0+。强跳过重组默认开启;不可跳过的条目可组合项在滚动时性能问题会被放大。
  • 用于性能测量的真实设备 + Release构建。skydoves观点#5:Debug构建会误导结果(Live Literals、解释执行模式)。准备测量时请查看
    ../../measurement/generating-baseline-profiles/SKILL.md

Workflow

工作流程

  • 1. Audit every
    items(...)
    call.
    Walk every
    LazyListScope.items(list)
    ,
    items(count)
    ,
    itemsIndexed(list)
    , and the
    LazyGridScope
    equivalents. For each, decide: does each element have a stable identity that outlives a single composition? If yes — and it almost always does — supply
    key = { it.id }
    using a server-side stable ID. MUST NOT use the list index,
    UUID.randomUUID()
    evaluated per emission, or
    hashCode()
    of a mutable object.
kotlin
// WRONG
LazyColumn { items(snacks) { snack -> SnackRow(snack) } }
// WRONG because: index-based identity → insert/remove discards composition state and breaks animateItem().
kotlin
// RIGHT
LazyColumn {
    items(
        items = snacks,
        key = { it.id },
        contentType = { it::class },
    ) { snack ->
        SnackRow(snack, Modifier.animateItem())
    }
}
  • 2. Add
    contentType
    for heterogeneous lists.
    Lazy layouts maintain a per-type composition cache analogous to RecyclerView's view-type. When item N + 1 has the same
    contentType
    as a recycled slot, the cached composition is reused; otherwise it is discarded and rebuilt. For homogeneous lists Compose infers a single content type and
    contentType
    is optional. For mixed feeds (cards, headers, ads, carousels, dividers) MUST supply a stable type discriminator.
  • 3. Validate item composable stability. Run
    ../../stability/diagnosing-compose-stability/SKILL.md
    . If the item composable accepts an
    unstable
    parameter, no amount of
    key
    /
    contentType
    work will help — the row recomposes on every scroll-driven snapshot tick anyway. Fix with
    ../../stability/stabilizing-compose-types/SKILL.md
    before tuning further.
  • 4. Hoist allocation-heavy values out of the items lambda. The items lambda runs once per item per scroll-driven (re)composition. Painters, color resolutions, shapes, and
    BorderStroke
    instances built inside the lambda are reallocated each pass. Hoist constants and
    remember
    -based caches above the
    LazyColumn
    or to the call site. Modifier chains are themselves cheap because Compose deduplicates them structurally — hoist a
    Modifier
    only when profiling proves it matters.
  • 5. Add
    Modifier.animateItem()
    for visual continuity.
    Pair with a stable
    key
    . The animation runs on inserts, removals, and reorders; without
    key
    the animation cannot bind to identity and silently no-ops. The default fade-in / fade-out / placement spring is usually correct; tune with
    fadeInSpec
    ,
    fadeOutSpec
    ,
    placementSpec
    only when the design system requires it.
  • 6. Cache common painters / colors / shapes outside the items block.
    painterResource(...)
    ,
    MaterialTheme.colorScheme.surface
    ,
    RoundedCornerShape(...)
    resolutions on every item composition add up. Hoist to the screen-level composable and pass down, or
    remember
    once at the
    LazyColumn
    parent.
  • 7. Verify with
    @TraceRecomposition
    and Layout Inspector.
    During a controlled scroll, expect each item composable to recompose at most once per real state change — not per scroll tick. Layout Inspector → Recomposition Counts column should plateau, not climb monotonically.
  • 1. 审核所有
    items(...)
    调用
    。检查每个
    LazyListScope.items(list)
    items(count)
    itemsIndexed(list)
    以及
    LazyGridScope
    的等效方法。对于每个调用,判断:每个元素是否拥有独立于单次组合的稳定标识?如果是(几乎所有场景都是),使用服务端稳定ID设置
    key = { it.id }
    严禁使用列表索引、每次发射时生成的
    UUID.randomUUID()
    或可变对象的
    hashCode()
kotlin
// 错误
LazyColumn { items(snacks) { snack -> SnackRow(snack) } }
// 错误原因:基于索引的标识 → 插入/删除操作会丢弃组合状态并破坏animateItem()的功能。
kotlin
// 正确
LazyColumn {
    items(
        items = snacks,
        key = { it.id },
        contentType = { it::class },
    ) { snack ->
        SnackRow(snack, Modifier.animateItem())
    }
}
  • 2. 为异构列表添加
    contentType
    。惰性布局维护基于类型的组合缓存,类似RecyclerView的视图类型。当第N+1个条目的
    contentType
    与回收槽的类型相同时,缓存的组合会被复用;否则会被丢弃并重新构建。对于同构列表,Compose会自动推断单一内容类型,
    contentType
    为可选参数。对于混合信息流(卡片、标题、广告、轮播图、分隔符)必须提供稳定的类型判别器。
  • 3. 验证条目可组合项的稳定性。运行
    ../../stability/diagnosing-compose-stability/SKILL.md
    。如果条目可组合项接收不稳定参数,无论
    key
    /
    contentType
    设置得多么完善都无济于事 — 每行仍会在每次滚动驱动的快照周期发生重组。在进一步调优前,请使用
    ../../stability/stabilizing-compose-types/SKILL.md
    修复稳定性问题。
  • 4. 将内存分配密集型值从items lambda中提升。items lambda会在每次滚动驱动的(重)组合时为每个条目执行一次。在lambda内部创建的Painter、颜色解析结果、形状和
    BorderStroke
    实例会在每次执行时重新分配。将常量和基于
    remember
    的缓存提升到
    LazyColumn
    上方或调用方。修饰符链本身开销很低,因为Compose会进行结构去重 — 只有当性能分析证明其是瓶颈时才需要提升
    Modifier
  • 5. 添加
    Modifier.animateItem()
    以保证视觉连续性
    。搭配稳定的
    key
    使用。动画会在插入、删除和重排时运行;如果没有
    key
    ,动画无法绑定到条目标识,会静默失效。默认的淡入/淡出/位移动画通常已满足需求;只有当设计系统有要求时,才需要通过
    fadeInSpec
    fadeOutSpec
    placementSpec
    进行调优。
  • 6. 在items块外部缓存通用Painter/颜色/形状。每次条目组合时解析
    painterResource(...)
    MaterialTheme.colorScheme.surface
    RoundedCornerShape(...)
    会累积性能开销。将这些提升到屏幕级可组合项并向下传递,或在
    LazyColumn
    父级通过
    remember
    缓存一次。
  • 7. 使用
    @TraceRecomposition
    和布局检查器验证
    。在受控滚动过程中,每个条目可组合项应最多在真实状态变化时重组一次 — 而非每次滚动周期。布局检查器 → 重组次数列应趋于平稳,而非持续增长。

Patterns

常见模式

Pattern: missing
key

模式:缺少
key

kotlin
// WRONG
LazyColumn {
    items(snacks) { snack -> SnackRow(snack) }
}
// WRONG because: items default to index-based identity. On insert/remove/reorder, every position past the change point has a different "identity", composition state and scroll-restoration are lost, and Modifier.animateItem() has nothing to animate from.
kotlin
// RIGHT
LazyColumn {
    items(snacks, key = { it.id }) { snack -> SnackRow(snack) }
}
kotlin
// 错误
LazyColumn {
    items(snacks) { snack -> SnackRow(snack) }
}
// 错误原因:条目默认使用基于索引的标识。插入/删除/重排时,变更点之后的所有位置的"标识"都会改变,组合状态和滚动恢复会丢失,Modifier.animateItem()也没有可动画的对象。
kotlin
// 正确
LazyColumn {
    items(snacks, key = { it.id }) { snack -> SnackRow(snack) }
}

Pattern: random or unstable key

模式:随机或不稳定的key

kotlin
// WRONG
items(snacks, key = { UUID.randomUUID() }) { snack -> SnackRow(snack) }
// WRONG because: a fresh key on every recomposition guarantees the cached composition is discarded every time — strictly worse than no key.
kotlin
// WRONG
items(snacks, key = { it.hashCode() }) { snack -> SnackRow(snack) }
// WRONG because: hashCode() of a mutable type changes when fields mutate, breaking identity continuity for the same logical item.
kotlin
// RIGHT
items(snacks, key = { it.id }) { snack -> SnackRow(snack) }
kotlin
// 错误
items(snacks, key = { UUID.randomUUID() }) { snack -> SnackRow(snack) }
// 错误原因:每次重组生成新的键会导致缓存的组合被频繁丢弃 — 这比不设置键的情况更糟。
kotlin
// 错误
items(snacks, key = { it.hashCode() }) { snack -> SnackRow(snack) }
// 错误原因:可变类型的hashCode()会随字段变化而改变,破坏同一逻辑条目的标识连续性。
kotlin
// 正确
items(snacks, key = { it.id }) { snack -> SnackRow(snack) }

Pattern: mixed feed without
contentType

模式:混合信息流未设置
contentType

kotlin
// WRONG
items(feed, key = { it.id }) { item ->
    when (item) {
        is FeedItem.Card -> CardRow(item)
        is FeedItem.Ad -> AdRow(item)
        is FeedItem.Header -> HeaderRow(item)
    }
}
// WRONG because: cached compositions of one type are discarded when scrolled into a different type's slot — every row crossing a type boundary is a fresh build instead of a recycled update.
kotlin
// RIGHT
items(
    items = feed,
    key = { it.id },
    contentType = { it::class },
) { item ->
    when (item) {
        is FeedItem.Card -> CardRow(item)
        is FeedItem.Ad -> AdRow(item)
        is FeedItem.Header -> HeaderRow(item)
    }
}
kotlin
// 错误
items(feed, key = { it.id }) { item ->
    when (item) {
        is FeedItem.Card -> CardRow(item)
        is FeedItem.Ad -> AdRow(item)
        is FeedItem.Header -> HeaderRow(item)
    }
}
// 错误原因:当不同类型的条目滚动到同一回收槽时,原类型的缓存组合会被丢弃 — 每个跨类型边界的条目都需要重新构建,而非复用更新。
kotlin
// 正确
items(
    items = feed,
    key = { it.id },
    contentType = { it::class },
) { item ->
    when (item) {
        is FeedItem.Card -> CardRow(item)
        is FeedItem.Ad -> AdRow(item)
        is FeedItem.Header -> HeaderRow(item)
    }
}

Pattern:
Modifier.animateItem()
without a stable key

模式:
Modifier.animateItem()
未搭配稳定key

kotlin
// WRONG
items(snacks) { snack ->
    SnackRow(snack, Modifier.animateItem())
}
// WRONG because: animateItem() binds animation state to the item's key. With no key, identity is index-based, so an insert at position 0 looks like every-row-changed and nothing animates correctly.
kotlin
// RIGHT
items(snacks, key = { it.id }) { snack ->
    SnackRow(snack, Modifier.animateItem())
}
kotlin
// 错误
items(snacks) { snack ->
    SnackRow(snack, Modifier.animateItem())
}
// 错误原因:animateItem()将动画状态绑定到条目的key。没有key时,标识基于索引,所以在位置0插入条目会被识别为所有行都发生变化,无法正确执行动画。
kotlin
// 正确
items(snacks, key = { it.id }) { snack ->
    SnackRow(snack, Modifier.animateItem())
}

Pattern: allocation inside the items lambda

模式:items lambda内部存在内存分配

kotlin
// WRONG
items(snacks, key = { it.id }) { snack ->
    val placeholder = painterResource(R.drawable.snack_placeholder)
    val border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
    Card(border = border) {
        AsyncImage(snack.imageUrl, placeholder = placeholder)
    }
}
// WRONG because: painterResource resolution and BorderStroke allocation happen on every item composition; at high scroll velocity these compound into measurable allocation pressure.
kotlin
// RIGHT
@Composable
fun SnackList(snacks: ImmutableList<Snack>) {
    val placeholder = painterResource(R.drawable.snack_placeholder)
    val border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
    LazyColumn {
        items(snacks, key = { it.id }, contentType = { it::class }) { snack ->
            Card(border = border) {
                AsyncImage(snack.imageUrl, placeholder = placeholder)
            }
        }
    }
}
Note: Compose deduplicates structurally-equal
Modifier
chains internally, so reallocating
Modifier.fillMaxWidth().padding(16.dp)
per item is a micro-optimization. Hoist a Modifier only when profiling identifies it as the bottleneck — premature
remember { Modifier.… }
adds noise without measurable benefit.
kotlin
// 错误
items(snacks, key = { it.id }) { snack ->
    val placeholder = painterResource(R.drawable.snack_placeholder)
    val border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
    Card(border = border) {
        AsyncImage(snack.imageUrl, placeholder = placeholder)
    }
}
// 错误原因:painterResource解析和BorderStroke分配会在每次条目组合时执行;高速滚动时这些操作会累积成可测量的内存分配压力。
kotlin
// 正确
@Composable
fun SnackList(snacks: ImmutableList<Snack>) {
    val placeholder = painterResource(R.drawable.snack_placeholder)
    val border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
    LazyColumn {
        items(snacks, key = { it.id }, contentType = { it::class }) { snack ->
            Card(border = border) {
                AsyncImage(snack.imageUrl, placeholder = placeholder)
            }
        }
    }
}
注意:Compose会在内部对结构相同的
Modifier
链进行去重,因此为每个条目重新创建
Modifier.fillMaxWidth().padding(16.dp)
属于微优化。只有当性能分析确定其为瓶颈时才需要提升Modifier — 过早使用
remember { Modifier.… }
只会增加代码复杂度,而无明显性能收益。

Pattern: unstable item composable swallows all gains

模式:不稳定的条目可组合项抵消所有优化效果

kotlin
// WRONG
@Composable
fun SnackRow(snack: Snack, tags: List<String>) { /* ... */ }

// Caller:
items(snacks, key = { it.id }) { snack ->
    SnackRow(snack, tags = snack.tags)
}
// WRONG because: List<String> is an unstable parameter under inference; every scroll-driven recomposition recomposes the row body even though the snack didn't change.
kotlin
// RIGHT
@Immutable
data class Snack(val id: Long, val name: String, val tags: ImmutableList<String>)

@Composable
fun SnackRow(snack: Snack) { /* ... */ }

items(snacks, key = { it.id }, contentType = { it::class }) { snack ->
    SnackRow(snack)
}
Cross-reference:
../../stability/stabilizing-compose-types/SKILL.md
.
kotlin
// 错误
@Composable
fun SnackRow(snack: Snack, tags: List<String>) { /* ... */ }

// 调用方:
items(snacks, key = { it.id }) { snack ->
    SnackRow(snack, tags = snack.tags)
}
// 错误原因:List<String>在默认推断下是不稳定参数;即使snack未变化,每次滚动驱动的重组仍会重新执行行体。
kotlin
// 正确
@Immutable
data class Snack(val id: Long, val name: String, val tags: ImmutableList<String>)

@Composable
fun SnackRow(snack: Snack) { /* ... */ }

items(snacks, key = { it.id }, contentType = { it::class }) { snack ->
    SnackRow(snack)
}
交叉参考:
../../stability/stabilizing-compose-types/SKILL.md

Pattern:
LazyVerticalGrid
with mixed spans

模式:包含混合跨度的
LazyVerticalGrid

kotlin
// RIGHT — keys + contentType apply to grids identically
LazyVerticalGrid(columns = GridCells.Fixed(2)) {
    items(
        items = feed,
        key = { it.id },
        contentType = { it::class },
        span = { item -> if (item is FeedItem.Header) GridItemSpan(maxLineSpan) else GridItemSpan(1) },
    ) { item ->
        when (item) {
            is FeedItem.Header -> HeaderRow(item, Modifier.animateItem())
            is FeedItem.Card -> CardCell(item, Modifier.animateItem())
        }
    }
}
kotlin
// 正确 — keys + contentType在网格布局中的应用方式完全相同
LazyVerticalGrid(columns = GridCells.Fixed(2)) {
    items(
        items = feed,
        key = { it.id },
        contentType = { it::class },
        span = { item -> if (item is FeedItem.Header) GridItemSpan(maxLineSpan) else GridItemSpan(1) },
    ) { item ->
        when (item) {
            is FeedItem.Header -> HeaderRow(item, Modifier.animateItem())
            is FeedItem.Card -> CardCell(item, Modifier.animateItem())
        }
    }
}

Mandatory rules

强制规则

  • MUST specify a
    key
    for every
    items(...)
    block where item identity outlives a single composition (effectively: every list backed by domain objects).
  • MUST use server-side stable IDs as keys. MUST NOT use the list index, MUST NOT use
    UUID.randomUUID()
    evaluated per emission, MUST NOT use
    hashCode()
    of a mutable object.
  • MUST specify
    contentType
    for heterogeneous lists (cards + headers + ads, etc.). Use a stable type discriminator such as
    it::class
    or a sealed
    enum
    .
  • MUST NOT use
    Modifier.animateItem()
    without a stable
    key
    — the animation silently no-ops.
  • MUST validate item composable stability with
    ../../stability/diagnosing-compose-stability/SKILL.md
    before blaming the lazy layout. An unstable item parameter cancels every gain from
    key
    /
    contentType
    .
  • MUST NOT wrap
    items { }
    in extra inline composable wrappers (
    Row { items { } }
    ) hoping to "force" skippability —
    Row
    /
    Column
    /
    Box
    are NOT restartable/skippable to begin with (skydoves hot take #3).
  • PREFERRED: combine with
    ../configuring-lazy-prefetch/SKILL.md
    for high-velocity scroll surfaces only after item-level fixes are in place.
  • PREFERRED: measure in release + R8 + on a real device (skydoves hot take #5) before declaring a fix complete.
  • 必须为每个
    items(...)
    块指定
    key
    ,只要条目标识独立于单次组合(实际上:所有基于域对象的列表)。
  • 必须使用服务端稳定ID作为key。严禁使用列表索引,严禁使用每次发射时生成的
    UUID.randomUUID()
    严禁使用可变对象的
    hashCode()
  • 必须为异构列表(卡片 + 标题 + 广告等)指定
    contentType
    。使用稳定的类型判别器,如
    it::class
    或密封
    enum
  • 严禁在未设置稳定
    key
    的情况下使用
    Modifier.animateItem()
    — 动画会静默失效。
  • 必须在质疑惰性布局性能前,使用
    ../../stability/diagnosing-compose-stability/SKILL.md
    验证条目可组合项的稳定性。不稳定的条目参数会抵消
    key
    /
    contentType
    带来的所有优化效果。
  • 严禁
    items { }
    包裹在额外的内联可组合包装器中(
    Row { items { } }
    )以试图"强制"跳过重组 —
    Row
    /
    Column
    /
    Box
    本身不支持重启/跳过重组(skydoves观点#3)。
  • 推荐:仅在完成条目级优化后,针对高速滚动界面结合使用
    ../configuring-lazy-prefetch/SKILL.md
  • 推荐:在声明修复完成前,在Release + R8 + 真实设备上进行性能测量(skydoves观点#5)。

Verification

验证标准

  • Reproduce the original scroll jank on a release build on a real device, then re-record after the fix; the dropped-frame rate measurably decreases.
  • Insert / remove / reorder operations preserve scroll position and per-item state (focus, expansion, scrubbed video position).
  • Modifier.animateItem()
    runs the expected fade and placement animation on inserts and removals.
  • Layout Inspector → Recomposition Counts column on item composables plateaus during steady scroll instead of climbing monotonically.
  • @TraceRecomposition
    on the item composable shows recompositions only on real state changes, not on every scroll-driven invalidation.
  • The compiler report (
    composables.txt
    ) shows the item composable as
    restartable skippable
    with all parameters
    stable
    or
    runtime
    .
  • 在真实设备的Release构建上重现原始滚动卡顿问题,修复后重新记录;丢帧率应显著降低。
  • 插入/删除/重排操作保留滚动位置和每条目状态(焦点、展开状态、视频播放进度)。
  • Modifier.animateItem()
    在插入和删除时运行预期的淡入淡出和位移动画。
  • 布局检查器 → 条目可组合项的重组次数列在稳定滚动时趋于平稳,而非持续增长。
  • 条目可组合项上的
    @TraceRecomposition
    显示仅在真实状态变化时发生重组,而非每次滚动驱动的失效时。
  • 编译器报告(
    composables.txt
    )显示条目可组合项为
    restartable skippable
    ,且所有参数为
    stable
    runtime

References

参考资料