compose-screens-footguns

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Compose screens footguns: layout, navigation and interaction

Compose 屏幕易错点:布局、导航与交互

32 traps mined from a production Kotlin and Compose Multiplatform app, one short file each under
references/
. Every file gives the working pattern, the Traps (the specific ways it fails in practice, and why), and a Verifying it section with commands to run against the user's own tree.
How to use this index. Match the code about to be written, or the symptom being chased, against the entries below: each names its topic and the symptom that should send you to it. Read every file that plausibly applies with the Read tool before proposing code or a fix; each is under 150 lines. Where a Verifying command names the source project's paths, substitute the equivalent paths in the user's tree.
Cross-references. A backticked trap name inside a file that is not listed here lives in a sibling area skill: glob
../*/references/<name>.md
to open it.
从生产级 Kotlin 与 Compose Multiplatform 应用中提炼出的 32 个陷阱,每个陷阱对应
references/
下的一个简短文件。每个文件都提供可行模式、陷阱(它在实际环境中的具体失败方式及原因),以及带有可针对用户项目目录树运行的命令的验证方式部分。
使用方法。 将即将编写的代码或正在排查的症状与以下条目进行匹配:每个条目都列出主题,以及应据此查阅该条目的症状。在提出代码或修复方案前,使用 Read 工具阅读所有可能适用的文件;每个文件不到 150 行。若验证命令指定了源项目的路径,请替换为用户项目目录树中的对应路径。
交叉引用。 文件中以反引号标出的、但此处未列出的陷阱名称位于同级区域 skill 中:使用 glob
../*/references/<name>.md
打开它。

Screens, navigation and adaptive layout

屏幕、导航与自适应布局

  • responsive-gate-size-not-platform
    — Gate an adaptive layout on the window's own width-versus-height, never on the platform, and flip every geometry value the gate owns in the same breath — frame sizing, content scale and scrim height. Use when a layout is right on a phone and wrong in a narrow or resized desktop window, when forcing an adaptive flag to one value breaks a different platform's screen, or when artwork swallows the whole page on a wide window.
  • nav-tab-registration-drift
    — A top-level tab has to be registered in every navigation surface that holds its own copy of the tab list — bottom bar, rail, and a stylized bar that keeps two lists — plus the graph and the flag that gates it, or the tab exists in code and never renders. The same drift catches a status entry point carried by more than one top app bar, where the missing badge reads as "nothing is running" rather than as a bug. Covers why a tab's ordinal is an identity rather than a position, what a conditional tab needs when it disappears under the user, and why a badge dot needs a ring of the page colour. Use when a newly added tab or badge shows on one surface but not another, when selection highlights the wrong tab after reordering, or when a tab bar overflows once one more tab appears.
  • type-safe-nav-graph-organization
    — Lay out a type-safe Navigation-Compose graph so it stays readable as the destination list grows — one serializable route object per file grouped by area, area graphs as extension functions on the graph builder, a single transition set on the host, and per-route theming applied by wrapping the screen inside its own entry. Use when a navigation file has grown to hundreds of lines, when route arguments start needing custom types, or when transitions or theming differ between destinations for no stated reason.
  • collapsing-parallax-toolbar
    — Build a collapsing header from five siblings in one box — four sharing a single scroll state, one driven by a boolean instead — with artwork moved by a graphics layer at half the scroll rate, a title interpolated along a two-segment curve into the pinned bar, and a derived flip point that swaps the floating back button for a real top bar. Use when a parallax header jitters or re-measures while scrolling, when the collapsing title drifts off its intended path, or when the pinned bar appears at the wrong scroll offset after a window resize.
  • fullscreen-video-gesture-overlay
    — The gesture and chrome layers of a fullscreen video screen — tap anywhere to toggle the controls, double-tap either half to seek, an auto-hide timer that every interaction postpones, and a picture-in-picture (PiP) guard that removes both layers. Use when single taps work on only half the screen, when the controls vanish while the user is dragging the seek bar, when the seek thumb snaps back under the finger, when a double-tap produces no ripple or a ripple that never fades, or when app chrome is still drawn over the video inside a small floating window.
  • nowplaying-pager-no-feedback-loop
    — A now-playing artwork pager that both follows the player and drives it, without the two writing to each other in a loop — the in-progress-scroll flag covers programmatic animation as well as drags, the seek is dispatched from the settled page only, and the page-difference decision is a pure function outside the UI runtime. Use when a swipe bounces back to the page it came from, when one swipe skips two tracks, when the pager stops following the player after a fast swipe, when swiping backwards restarts the current track instead of going back one, or when a far swipe under shuffle lands on the wrong song.
  • layered-video-backdrop-pager
    — Building a swipeable now-playing page whose layers — a colour backdrop, a full-bleed looping video or image, and a centred square cover — all belong to one pager, with per-page colours taken from the bitmap that page actually painted and a scrim spanning the whole page. Use when a swipe makes the background and the cover slide out of step, when video from one track bleeds into the neighbouring page mid-swipe, when the backdrop colour belongs to the wrong track or flashes black between pages, when the bottom of the video shows through the controls, or when a tap overlay stops the pager from being dragged at all.
  • desktop-mini-player-window
    — A second always-on-top, frameless desktop window for playback controls — its existence held as one boolean outside the composition, the same state object as the main window, a hand-rolled drag that anchors to absolute pointer coordinates, a native minimum size in device pixels, and geometry persisted without flooding the store. Use when the small window drifts or jitters while being dragged, when it cannot be resized past a corner, when it collapses below its content, when it disappears the moment the main window is closed, when it opens invisible, or when its state disagrees with the main window's.
  • screen-shell-content-split
    — Split one screen into a shell that owns every cross-look concern and a content layer that only renders, connected by two holders — a state snapshot and an actions bag — so adding a second look costs one branch instead of a parameter-list edit; covers why the holders are stable-but-not-immutable, what must stay in the shell, and what must not. Use when a screen has grown a second visual style, when a style switch resets the scroll position or the artwork page, when adding a look means editing a fifty-argument signature, or when a "shared" helper starts needing a per-style
    if
    .
  • variant-layout-math-stays-in-the-variant
    — When two looks of one screen each need a fit-exactly-one-screen measurement — measure the fixed blocks, split the remainder into equal gaps, floor it at a minimum — keep a copy per look instead of hoisting one; covers the effect keys the block needs, the invisible spacer that must mirror the ratio actually drawn, and which spacer may animate. Use when the gap above or below a hero element keeps last track's size, when content that should end at the fold overflows or leaves a band of dead space, or before extracting "the same" layout maths from two screens into one helper.
  • empty-state-must-keep-its-navigation
    — Replace a populated header with an empty-state message without deleting the controls that header owned — re-supply them only in the branch that owned them, order the loading branch above the empty one, and stop reserving the artwork's height for a line of text. Use when a user reaches an empty period, filter or search result and cannot get back out, when an empty message flashes on every reload, or when the same control renders twice.
  • stacked-bars-double-consume-window-insets
    — Inset consumption travels to a composable's descendants and never to its siblings, so two inset-aware bars stacked in one column each reserve the system bar and open a band of dead space exactly one bar tall. Covers parameterising a bar's
    windowInsets
    with the framework default, deciding once who consumes, and why the same component must keep the default at its overlay call sites. Use when a strip of empty space appears between two bars, when it only shows in one mode of a screen, or when a bar's leading icon is clipped in landscape after you zeroed its insets.
  • floating-overlay-reserves-its-own-strip
    — A control floated over a scrolling column is a sibling that takes part in no measurement, so the column has to reserve the strip it covers explicitly — as a leading spacer, or as a header row whose twin spacers keep a centred title centred over the hole. Covers what the strip constant may and may not count, why every scrolling branch needs its own, and why floating is what keeps the control reachable once the page has scrolled. Use when a floating back button sits on top of the first line of content, when a centred title is off by half a button, or when a control scrolls out of reach on a long page.
  • responsive-gate-size-not-platform
    — 应根据窗口自身的宽高关系,而不是平台,来决定自适应布局分支,并在切换时同步调整该分支控制的所有几何值——框架尺寸、内容缩放和遮罩高度。当布局在手机上正确、在狭窄或调整过大小的桌面窗口中错误,强制将自适应标志设为某个值导致其他平台的屏幕出错,或宽窗口中的插图占满整页时使用。
  • nav-tab-registration-drift
    — 顶层标签页必须注册到每个持有自身标签列表的导航界面中——底部栏、导航侧边栏,以及维护两套列表的样式化栏——还要注册到导航图和控制它的标志中,否则标签页存在于代码中却永远不会呈现。相同的漂移也会影响由多个顶层应用栏承载的状态入口;缺失的徽标会被理解为“没有运行中的内容”,而不是 bug。涵盖标签页的序号为何是身份而非位置、条件标签页在用户端消失时需要什么,以及徽标圆点为何需要用页面颜色描边。当新增标签页或徽标只显示在一个界面上、重排后高亮错误标签页,或多一个标签页导致标签栏溢出时使用。
  • type-safe-nav-graph-organization
    — 以可读性为优先组织类型安全的 Navigation-Compose 导航图,随着目的地列表增长仍保持清晰——按区域分组,每个文件定义一个可序列化路由对象,以扩展函数在 graph builder 上定义区域导航图,在宿主上设置单一过渡集合,并通过在各条目中包裹相应屏幕来应用按路由配置的主题。当导航文件增长到数百行、路由参数开始需要自定义类型,或不同目的地之间的过渡和主题无明确理由却不同时使用。
  • collapsing-parallax-toolbar
    — 在同一个 Box 中使用五个同级元素构建折叠式页眉——四个共享同一滚动状态,一个改由布尔值驱动——通过以一半滚动速率移动的 graphics layer 处理插图,沿两段曲线将标题插值到固定栏,并使用推导出的翻转点将浮动返回按钮替换为真正的顶部栏。当视差页眉在滚动时抖动或反复测量、折叠标题偏离预期路径,或窗口调整大小后固定栏在错误的滚动偏移处出现时使用。
  • fullscreen-video-gesture-overlay
    — 全屏视频屏幕的手势层和控件层——点击任意位置切换控件、双击任一半屏进行定位、每次交互都会延后自动隐藏计时器,以及移除这两个层的画中画(PiP)保护机制。当单次点击仅在屏幕一半有效、拖动进度条时控件消失、拖动时 seek thumb 在手指下弹回、双击没有涟漪或涟漪始终不消失,或小浮动窗口内应用控件仍绘制在视频上方时使用。
  • nowplaying-pager-no-feedback-loop
    — 构建一个既跟随播放器又驱动播放器的正在播放封面 Pager,同时避免二者相互写入形成循环——进行中滚动标志要涵盖程序化动画和拖动,seek 只在页面稳定后派发,页面差异判断则是 UI 运行时之外的纯函数。当滑动后弹回原页面、一次滑动跳过两首曲目、快速滑动后 Pager 不再跟随播放器、反向滑动会重新播放当前曲目而非上一首,或随机播放时远距离滑动落到错误歌曲时使用。
  • layered-video-backdrop-pager
    — 构建可滑动的正在播放页面,其各层——彩色背景、全屏循环视频或图片、居中的方形封面——都属于同一个 Pager,每页颜色取自该页实际绘制的 bitmap,并让 scrim 覆盖整页。当滑动导致背景与封面不同步、滑动过程中一首曲目的视频渗入相邻页面、背景颜色属于错误曲目或页面间闪黑、视频底部透过控件显示,或点击 overlay 阻止 Pager 拖动时使用。
  • desktop-mini-player-window
    — 为播放控制创建第二个始终置顶、无边框的桌面窗口——其存在状态由 composition 外部的单个布尔值维护,与主窗口共享同一状态对象,手工实现的拖动锚定到绝对指针坐标,原生最小尺寸以设备像素计,且持久化几何信息时不会让 store 泛滥。当小窗口拖动时漂移或抖动、无法通过某个角部进一步缩放、折叠到内容以下、主窗口关闭时立即消失、打开时不可见,或其状态与主窗口不一致时使用。
  • screen-shell-content-split
    — 将一个屏幕拆分为负责所有跨外观关注点的 shell 和只负责渲染的内容层,通过两个 holder——状态快照和 actions bag——连接,使新增第二种外观只需增加一个分支,而不是修改参数列表;涵盖 holder 为何应稳定但非不可变、哪些内容必须留在 shell 中,以及哪些内容不应留在其中。当屏幕新增第二种视觉样式、切换样式会重置滚动位置或 artwork 页面、新增外观意味着编辑一个包含五十个参数的签名,或某个“共享”helper 开始需要按样式分支时使用。
  • variant-layout-math-stays-in-the-variant
    — 当同一屏幕的两种外观都需要“恰好适配一屏”的测量时——测量固定区块,将剩余空间拆成等宽间距,并确保不低于最小值——应为每种外观保留一份计算逻辑,而不是将其提升到外层;涵盖该区块需要哪些 effect key、隐形占位符必须镜像实际绘制的比例,以及哪个占位符可以动画。当 hero 元素上方或下方的间距保持为最后一个 track 的大小、应止于折叠处的内容溢出或留下死区,或在将两个屏幕中“相同”的布局计算提取到某个 helper 之前,使用此方法。
  • empty-state-must-keep-its-navigation
    — 用空状态消息替换已填充状态的页眉时,不要删除该页眉原本拥有的控件——只应在原本拥有这些控件的分支中重新提供它们,将加载分支置于空状态分支之上,并停止为一行文本预留 artwork 的高度。当用户到达空时段、筛选或搜索结果后无法退出,空消息在每次重新加载时闪现,或同一控件被渲染两次时使用。
  • stacked-bars-double-consume-window-insets
    — Inset 的消费会传递给 composable 的后代,而不会传递给同级元素,因此同一列中堆叠的两个感知 Inset 的栏都会各自预留系统栏空间,从而产生恰好一条栏高的空白带。涵盖如何用框架默认值参数化栏的
    windowInsets
    、如何一次性决定由谁消费,以及为什么同一组件在 overlay 调用点必须保留默认值。当两个栏之间出现空白带、它只在屏幕某一种模式下出现,或将 Insets 归零后横向模式下栏的前导图标被裁切时使用。
  • floating-overlay-reserves-its-own-strip
    — 悬浮在滚动列上方的控件是同级元素,不参与测量,因此列必须显式预留它覆盖的区域——可使用前导 spacer,或使用带有成对 spacer 的标题行,使居中标题仍以空洞为中心并保持居中。涵盖覆盖条常量可以计入和不可计入的内容、为什么每个滚动分支都需要自己的预留,以及为什么正是悬浮特性让控件在页面滚动后仍可触达。当浮动返回按钮盖住内容第一行、居中标题偏离半个按钮宽度,或控件在长页面滚动后移出可触达范围时使用。

Components and interaction

组件与交互

  • lazy-list-drag-reorder
    — A complete drag-to-reorder state holder for a lazily composed Compose list — pointer offset accumulation, target-index math over the visible window, how the lift animation and the built-in item placement animation must not overlap, edge auto-scroll as a delta the caller drives, and the commit-on-drop contract with the data layer. Use when building reorder, or when a dragged row snaps back to its old slot, jitters as the list re-lays-out under it, commits a move that the user cancelled, or scrolls the list instead of moving the row.
  • swipe-action-list-row
    — One list row carrying three gestures at once — tap, long-press-to-select, and swipe-sideways-for-an-action — plus a mode flag that changes what the tap means. Covers the pointerInput key that decides whether the swipe detector sees the current mode or the one captured at composition, translating the row in the layout phase, latching the commit threshold, and leaving the opposite drag direction to the parent. Use when a row keeps swiping after multi-select has started, when selection only begins working after the row happens to recompose, or when a swipe fights the pager or list underneath.
  • selection-mode-state-holder
    — A small @Stable holder for multi-select in a Compose list — keyed by stable id rather than list position, with every mutation funnelled through one private method so a hard cap on the selection size cannot be bypassed, plus the toggle/clear/select-all semantics that make the two gestures read differently. Use when building bulk actions over a list, when selections drift onto the wrong rows after a reorder or a page load, or when a cap holds for tapping rows but not for select-all.
  • expandable-and-linkified-text
    — Clamp long text to a few lines with a more/less affordance driven by measured overflow instead of a character count, and make timestamps or URLs inside the same text tappable. Use when "more" shows on text that already fits, never shows on text that does not, gets truncated along with the text, or when tapping a link expands the block instead of following the link.
  • lazy-scroll-helper-kit
    — Four small lazy-list utilities worth carrying between apps — scroll-direction as derived state, centre-an-item scrolling that waits a frame before measuring, an item's visible percentage, and a lookup into the visible window — with the trap each one hides. Use when a hide-on-scroll bar flickers or sticks, when scrolling to an item lands it at the edge or does nothing, or when viewport arithmetic returns values for the wrong item.
  • custom-thin-media-slider
    — A slim seek bar with a buffered-progress track behind it, built from Material3's Slider with custom track and thumb slots — including the fraction-not-your-own-scale rule that keeps the thumb from pinning at the end, and the state gate that stops incoming playback position from fighting the drag. Use when a seek bar renders full or empty regardless of position, when the thumb snaps back while dragging, when a thin control refuses to get thinner, or when stray dots and ticks appear on the track.
  • custom-modal-sheet-family
    — A house style over Material3's ModalBottomSheet — transparent container plus your own surface, zeroed window insets with an explicit end spacer, a hand-rolled drag handle, and hide-then-dismiss so the sheet animates closed before it leaves composition. Use when a sheet snaps shut instead of sliding, when its last row sits under the navigation bar, when text inside it is invisible, or when a family of sheets has drifted into a family of slightly different sheets.
  • control-range-must-cover-stored-values
    — Size a slider's range from the real distribution of values it will be handed — stored, imported, migrated — rather than from a neighbouring control's range, because a value outside the range parks the thumb at the end of the track while your readout shows a different number, and the first touch silently rewrites it. Use when adding a slider for a value that can arrive from anywhere but the slider itself.
  • edit-a-shape-as-a-shape
    — When the value being edited is a curve, draw a draggable curve instead of N sliders and embed it in the settings list instead of pushing a screen — with a raw pointer loop rather than a drag-gesture helper, a draft that commits once per gesture, and smoothing that never overshoots a handle the user placed. Use when building a multi-point editor, or when a curve control ignores taps, snaps back on release, or wipes the saved value.
  • progress-indicator-as-scrubber
    — Build a seek control from a progress indicator plus a transparent pointer layer instead of from a slider — a hit box taller than the visual, drags consumed so an ancestor pager cannot steal a scrub, the drag's own fraction shown while interacting, a thumb drawn by you, and the separate decision of drawing an element closer without moving its layout slot. Use when the visual you need has no slider equivalent, when a scrub gets hijacked by a swipe or a sheet, when the bar jumps back mid-drag, or when a thin control is impossible to hit.
  • scoped-composable-shadows-the-top-level-one
    — Inside a layout scope, a scope-extension composable of the same name wins over the top-level one — silently, since both compile — so a call written for the plain version gets the scoped version's defaults and layout behaviour; covers fully qualifying to force the top-level one,
    this@Scope.
    to force the scoped one, and why outer scopes still apply from inside a nested layout. Use when an appear/disappear animation expands or collapses its parent instead of fading in place, when a composable behaves differently after being moved into a column or row, or when a call resolves to an overload you did not choose.
  • settings-value-round-tripped-through-its-label
    — A selection dialog that maps the chosen localized label back to a stored value breaks the day two labels translate identically — the write is skipped or lands on the wrong option, with no error; covers carrying the id instead of the text, making the miss loud, and the sibling hazard of a default declared both in the store and as the collector's initial value. Use when a settings choice does not stick in one language only, when a picker writes a neighbouring option, or when a screen renders the wrong variant for a moment on entry.
  • self-hiding-child-cannot-hide-its-slot
    — A component that renders nothing when its feature is unavailable cannot remove the container someone else wrapped it in — gate the slot on the same availability predicate, publish that predicate beside the component, and branch the slot's clickable and non-clickable forms properly. Use when an empty cell, gap or stray divider appears where an optional control should be, when a group's rounded end caps land on the wrong item, or when a wrapper swallows the taps meant for the control inside it.
  • event-not-state-edge-for-one-shot-effects
    — Fire a one-shot celebration effect from the click that caused it, never from the state that click produced — an effect watching a boolean for a false→true edge cannot tell a tap from data arriving a beat later, so moving to an already-marked record fires the same edge. Covers the @Stable holder that owns live effects, judging the meaning at tap time, why the previous-value variable is a second source of truth, and keeping concurrent effects additive. Use when an effect plays by itself while skipping between records, when it celebrates something the user did not do, or when the first genuine tap after a screen returns is silently swallowed.
  • weight-fill-false-to-center-a-cluster
    — A weighted child occupies its whole slot even when its content measures narrower, which pins the sibling beside it to the far edge;
    weight(1f, fill = false)
    releases the unused width back to the row's arrangement so child, gap and sibling read as one cluster. Covers why the tell is the sibling rather than the weighted child, why the fix is two edits and not one, and why dropping the weight instead is a different layout. Use when a bar looks centred at one content size and lopsided at another, when a button clings to the screen edge with a hole beside it, or when a trailing label sits far from the text it belongs to.
  • lazy-item-grouping-beats-arrangement-gap
    — A lazy list's
    spacedBy
    arrangement applies between every pair of items and compounds with each item's own edge padding, so blocks that must read as one unit belong in ONE item carrying its own tighter spacing rather than in three items relying on the list's gap. Covers why the visible gap appears in no single constant, why an item boundary is a spacing boundary, and what to do when the group is conditional. Use when a band of dead space opens above one block, when tightening the gap for one pair moves every other pair, or before splitting a header into separate lazy items.
  • commit-a-text-field-on-an-explicit-action
    — An editable value that is written on focus loss stores whatever was half-typed when a dialog, a rotation or a stray tap took the focus away; keep the draft in its own state keyed on the stored value and write only when the user asks for it. Covers why the dirty check that shows the confirm button gets stuck when the commit normalises, where the draft should live, and what a second commit path has to agree with. Use when a setting holds a truncated value nobody typed, when a Save button never goes away after saving, or when an externally changed value does not reach the field.
  • story-reel-auto-pager
    — A story-style reel — a pager that advances itself on a per-card timer, with a segmented progress bar, tap zones to skip forward or back, and a press-and-hold that pauses it — where every card's data is computed once before the first frame instead of card by card, and the segmented bar's count comes from a card list some years never fill completely. Covers a frame-delta timer that a long hold cannot bank progress against, why an empty onLongPress callback is what makes hold-then-release resume instead of navigate, reading the pager's target page rather than its current one inside a tap handler, and pinning a captured card's colour scheme so it renders the same regardless of the viewer's own theme. Use when a reel's progress bar jumps to the wrong segment, when releasing a paused hold immediately skips a card instead of resuming it, when a loading spinner appears mid-story instead of only before the first card, or when a shared card looks different depending on which theme the device was in.
  • hidden-setting-does-not-clear-the-stored-value
    — A style or effect option is correctly hidden from a settings picker below some OS version or capability floor, yet the effect it names still shows up broken — flat, unblurred, or simply wrong — on a device that should never be able to select it, because the picker gates what a user can choose next, not what a stored preference already holds. Use when a version-gated visual feature has a capability check in one place but the bug still reproduces, or when adding an expect/actual boolean for a modifier that fails silently instead of throwing.
  • lazy-list-drag-reorder
    — 为惰性组合的 Compose 列表实现完整的拖拽重排状态 holder——指针偏移累加、基于可见窗口计算目标索引、抬升动画与内置项位移动画不得重叠、作为调用方驱动的增量进行边缘自动滚动,以及与数据层约定的放下时提交。当构建重排功能,或被拖拽行弹回旧位置、在列表重排时抖动、提交了用户已取消的移动,或滚动列表而非移动行时使用。
  • swipe-action-list-row
    — 让一个列表行同时承载三种手势——点击、长按选择和侧滑执行操作——并用模式标志改变点击的含义。涵盖决定滑动检测器看到当前模式还是组合时捕获模式的 pointerInput key、在布局阶段平移行、锁存提交阈值,以及将相反的拖拽方向留给父级。当多选开始后行仍持续滑动、选择只有在行偶然重组后才生效,或滑动与下层 Pager 或列表争夺手势时使用。
  • selection-mode-state-holder
    — 为 Compose 列表中的多选使用小型
    @Stable
    holder——按稳定 id 而非列表位置标识,所有变更都汇入一个私有方法,使选择数量硬上限无法被绕过,再通过切换、清空和全选语义让两种手势呈现不同含义。当在列表之上构建批量操作、重排或分页加载后选择漂移到错误行,或点击行时上限生效但全选时失效时使用。
  • expandable-and-linkified-text
    — 将长文本限制在几行,并用基于测量到的溢出而非字符数驱动的“更多/较少”控件;让同一文本中的时间戳或 URL 可点击。当“更多”出现在本来已完整显示的文本上、本应显示却不出现、和文本一起被截断,或点击链接会展开整个区块而非跳转链接时使用。
  • lazy-scroll-helper-kit
    — 四项值得在不同应用间复用的 lazy-list 工具——将滚动方向作为派生状态、将某项滚动到中心并在测量前等待一帧、计算项的可见百分比,以及查找可见窗口——每一项都隐藏一个陷阱。当随滚动隐藏的栏闪烁或卡住、滚动到某项时停在边缘或毫无反应,或视口计算返回了错误项的值时使用。
  • custom-thin-media-slider
    — 构建一条纤薄的媒体进度条,在其后方放置缓冲进度轨道,基于 Material3 的 Slider 并自定义轨道和 thumb 插槽——包括“不要用自身 scale 计算分数”的规则,防止 thumb 固定在末端,以及用状态门控阻止传入的播放位置与拖动相争。当无论位置如何进度条都显示为满或空、拖动时 thumb 弹回、纤薄控件无法变得更薄,或轨道上出现多余圆点和刻度时使用。
  • custom-modal-sheet-family
    — 在 Material3 的 ModalBottomSheet 之上构建统一风格——透明容器加上自定义 surface,将 window insets 归零并添加显式末尾 spacer,手工实现拖动把手,并采用先隐藏再关闭的方式,让 sheet 在离开 composition 前完成关闭动画。当 sheet 猛然合上而非滑动关闭、最后一行位于导航栏下方、其中文字不可见,或一系列 sheet 逐渐变成彼此略有差异的一组 sheet 时使用。
  • control-range-must-cover-stored-values
    — 应根据 Slider 实际接收到的值分布——已存储、导入或迁移而来——而非相邻控件的范围,来确定 Slider 范围,因为超出范围的值会让 thumb 停在轨道末端,而读数却显示不同数字,并且首次触摸会悄然重写该值。当为可能从 Slider 之外任何来源到达的值添加 Slider 时使用。
  • edit-a-shape-as-a-shape
    — 当被编辑的值是一条曲线时,应绘制可拖拽曲线,而不是使用 N 个 Slider,并将其嵌入设置列表而非推入独立屏幕——使用原始指针循环而非 drag-gesture helper,每次手势只提交一次草稿,并确保平滑处理不会越过用户放置的控制点。当构建多点编辑器,或曲线控件忽略点击、释放后弹回,或清除已保存值时使用。
  • progress-indicator-as-scrubber
    — 使用进度指示器加透明指针层来构建 seek 控件,而不是使用 Slider——命中框高于视觉尺寸,消费拖动事件以防祖先 Pager 抢走 scrub,交互时显示拖动自身的比例,由你自行绘制 thumb,并将“让元素绘制得更靠前”与“移动其布局槽位”分开决定。当所需视觉没有对应的 Slider、scrub 被滑动或 Sheet 抢占、拖动中途进度条跳回,或纤薄控件无法命中时使用。
  • scoped-composable-shadows-the-top-level-one
    — 在布局 scope 内,同名的 scope-extension composable 会优先于顶层版本——而且是静默生效,因为两者都能编译——因此为普通版本编写的调用会获得 scope 版本的默认行为和布局逻辑;涵盖如何完全限定名称以强制使用顶层版本、如何用
    this@Scope.
    强制使用 scoped 版本,以及为什么从嵌套布局内部调用时,外层 scope 仍然适用。当出现或消失动画会扩大或折叠父容器而不是原地淡入淡出、composable 移入 column 或 row 后行为发生变化,或调用解析到了非预期重载时使用。
  • settings-value-round-tripped-through-its-label
    — 将所选的本地化 label 映射回存储值的选择对话框,会在两个 label 恰好翻译为相同文本时失效——写入会被跳过或落到错误选项,且不报错;涵盖应携带 id 而非文本、如何让未匹配显式报错,以及在 store 和 collector 初始值中同时声明默认值这一同类隐患。当某个设置选项仅在一种语言中无法保存、picker 写入相邻选项,或进入屏幕时短暂渲染了错误变体时使用。
  • self-hiding-child-cannot-hide-its-slot
    — 当某项功能不可用而渲染为空的组件,无法移除其他人包裹它的容器——应使用同一可用性判断条件控制槽位,在组件旁发布该判断条件,并正确分支处理槽位的可点击和不可点击形式。当可选控件所在位置出现空单元格、空隙或多余分隔线、某组的圆角端盖落在错误项上,或 wrapper 吞掉本应传递给内部控件的点击时使用。
  • event-not-state-edge-for-one-shot-effects
    — 一次性庆祝效果必须由触发它的点击触发,绝不能由该点击产生的状态触发——监视布尔值 false→true 边沿的效果无法区分用户点击与稍后到达的数据,因此切换到已标记记录也会触发相同边沿。涵盖管理实时效果的
    @Stable
    holder、在点击时判断含义、为何 previous-value 变量会成为第二真相来源,以及如何让并发效果保持可叠加。当跳读记录时效果自行播放、庆祝了用户未执行的操作,或返回屏幕后的首次真实点击被静默吞掉时使用。
  • weight-fill-false-to-center-a-cluster
    — 带权子项即使内容测量宽度更窄,也会占据整个槽位,从而把旁边的同级项推到边缘;
    weight(1f, fill = false)
    会将未使用的宽度释放回 row 的排列方式,使子项、间距和同级项形成一个整体。涵盖判断线索为何是同级项而非带权子项、为何修复需要两处编辑而非一处,以及直接去掉 weight 为何是另一种布局。当栏在一种内容尺寸下看似居中、在另一种尺寸下却偏斜,按钮紧贴屏幕边缘且旁边留有空洞,或尾部标签远离其所属文本时使用。
  • lazy-item-grouping-beats-arrangement-gap
    — Lazy list 的
    spacedBy
    arrangement 会应用于每两个相邻项之间,并与每个项自身的边缘 padding 叠加,因此必须作为一个整体阅读的区块应放在一个 item 中,由它自行采用更紧凑的间距,而不是拆成三个 item 并依赖列表间距。涵盖为何可见间距不出现在任何单一常量中、为何 item 边界也是间距边界,以及条件分组应如何处理。当某个区块上方出现死区、收紧一对项的间距会同时影响其他所有间距,或将 header 拆成独立 lazy items 之前,使用此方法。
  • commit-a-text-field-on-an-explicit-action
    — 在失去焦点时写入的可编辑值,会保存对话框、旋转或误触导致焦点离开时尚未输入完的内容;应让草稿以存储值为 key 保存在独立 state 中,并只在用户明确操作时写入。涵盖用于显示确认按钮的 dirty 判断为何会在提交规范化后卡住、草稿应存放在哪里,以及第二条提交路径必须遵循什么。当某项设置保存了无人输入过的截断值、保存后 Save 按钮始终不消失,或外部变更的值未传到输入框时使用。
  • story-reel-auto-pager
    — 构建 story 风格的 reel——一个按每张卡片独立计时器自动翻页的 Pager,带分段进度条、用于前后跳转的点击区域以及可暂停它的长按——其中每张卡片的数据都在首帧之前一次性计算完成,而不是逐卡计算;分段进度条的数量取自一个可能多年都无法完全填满的卡片列表。涵盖长按无法为其累计进度的帧 delta 计时器、为何空的 onLongPress 回调能让“按住后释放”恢复而不是导航、在点击处理器内读取 Pager 的目标页而非当前页,以及固定捕获卡片的 color scheme,使其无论查看者主题如何都保持一致。当 reel 的进度条跳到错误分段、释放暂停的长按后立即跳过卡片而非恢复、故事中途而非仅首张卡片前出现加载 spinner,或共享卡片因设备主题不同而外观不同时使用。
  • hidden-setting-does-not-clear-the-stored-value
    — 某项 style 或 effect 选项在低于特定 OS 版本或能力门槛时已正确从设置 picker 中隐藏,但该选项命名的效果仍会错误显示——扁平、未模糊,或单纯效果不对——在不应能够选择它的设备上,因为 picker 只控制用户接下来能选择什么,而不控制已存储的 preference。当版本门控的视觉功能只在一处进行了能力检查却仍能复现问题,或为某个静默失败而非抛错的 modifier 添加 expect/actual 布尔值时使用。