compose-focus-navigation
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCompose: focus navigation
Compose:焦点导航
Core principle
核心原则
Focus is stateful UI behavior. Make focus targets explicit, request focus after composition succeeds, and test navigation with the same input model users use: keyboard, D-pad, or remote keys.
焦点是有状态的UI行为。需明确焦点目标,在组合完成后请求焦点,并使用与用户相同的输入模型测试导航:键盘、方向键(D-pad)或遥控器按键。
When to use this skill
何时使用此技能
Use this when UI:
- Runs on TV, desktop, ChromeOS, keyboard-first Android, or remote-control devices.
- Uses ,
FocusRequester,focusRequester,focusProperties, or key handlers.onFocusChanged - Needs initial focus, restored focus, directional navigation, or back/escape behavior.
- Has a carousel, grid, lazy list, menu, dialog, or modal with focus traps.
- Has tests asserting which item is focused.
在以下UI场景中使用:
- 运行在电视、桌面端、ChromeOS、优先使用键盘的Android设备或遥控器控制设备上。
- 使用了、
FocusRequester、focusRequester、focusProperties或按键处理器。onFocusChanged - 需要初始焦点、焦点恢复、定向导航或返回/退出行为。
- 包含轮播图、网格、懒加载列表、菜单、对话框或带焦点陷阱的模态框。
- 存在断言哪个项获得焦点的测试用例。
Build focus targets deliberately
刻意构建焦点目标
Start with components that already participate in focus, then add only the focus hooks the behavior needs:
| Need | Add |
|---|---|
| Normal button/text field/clickable focus | Nothing extra; use the focusable component |
| Programmatic initial/restored focus | |
| Visual or state reaction to focus changes | |
| Custom interactive surface that is not already focusable | |
For example, request and observe focus only when both behaviors are needed:
kotlin
val requester = remember { FocusRequester() }
Button(
onClick = onClick,
modifier = Modifier
.focusRequester(requester)
.onFocusChanged { state -> isFocused = state.isFocused },
) {
Text("Play")
}Prefer focusable components (, , clickable/selectable surfaces) over manually adding to passive layout. Add manual focus only when the element is truly interactive or participates in navigation.
ButtonTextFieldfocusable()从已支持焦点的组件开始,仅添加行为所需的焦点钩子:
| 需求 | 需添加的内容 |
|---|---|
| 普通按钮/文本框/可点击组件的焦点 | 无需额外操作;使用可聚焦组件 |
| 程序化初始/恢复焦点 | |
| 对焦点变化的视觉或状态响应 | |
| 本身不可聚焦的自定义交互界面 | |
例如,仅在同时需要两种行为时才请求并观察焦点:
kotlin
val requester = remember { FocusRequester() }
Button(
onClick = onClick,
modifier = Modifier
.focusRequester(requester)
.onFocusChanged { state -> isFocused = state.isFocused },
) {
Text("Play")
}优先使用可聚焦组件(、、可点击/可选界面),而非手动给被动布局添加。仅当元素真正具有交互性或参与导航时,才手动添加焦点。
ButtonTextFieldfocusable()Request focus after composition
组合完成后请求焦点
Call focus requests from an effect, not from the composable body:
kotlin
val initialFocus = remember { FocusRequester() }
LaunchedEffect(initialFocus) {
initialFocus.requestFocus()
}If the target appears after loading, key the request to the condition:
kotlin
LaunchedEffect(items.isNotEmpty()) {
if (items.isNotEmpty()) {
firstItemRequester.requestFocus()
}
}For lazy content, request focus only after the item is actually composed. Keep requesters in stable item state keyed by item id, not by index alone if the list can reorder.
从effect中调用焦点请求,而非在可组合函数体内:
kotlin
val initialFocus = remember { FocusRequester() }
LaunchedEffect(initialFocus) {
initialFocus.requestFocus()
}如果目标在加载后才出现,将请求与条件关联:
kotlin
LaunchedEffect(items.isNotEmpty()) {
if (items.isNotEmpty()) {
firstItemRequester.requestFocus()
}
}对于懒加载内容,仅在项实际组合完成后请求焦点。将请求器存储在由项ID标记的稳定项状态中,若列表可重新排序,则不要仅通过索引存储。
Directional navigation
定向导航
Use when default spatial search is wrong:
focusPropertieskotlin
Modifier.focusProperties {
up = headerRequester
down = firstRowRequester
left = FocusRequester.Cancel
}Use this sparingly. Too many hard-coded links create stale focus graphs when layouts change. Prefer natural focus order unless the design requires a specific jump or trap.
当默认空间搜索不符合需求时,使用:
focusPropertieskotlin
Modifier.focusProperties {
up = headerRequester
down = firstRowRequester
left = FocusRequester.Cancel
}谨慎使用此方式。过多硬编码链接会在布局变化时导致焦点图失效。除非设计需要特定跳转或陷阱,否则优先使用自然焦点顺序。
Key events
按键事件
Use key handlers for behavior that is not normal click/focus traversal:
kotlin
Modifier.onPreviewKeyEvent { event ->
if (event.type == KeyEventType.KeyUp && event.key == Key.Back) {
onBack()
true
} else {
false
}
}Return only when consumed. Returning too broadly breaks text entry, accessibility shortcuts, and parent navigation.
truetrueFor rapid D-pad input, throttle at the boundary that owns the expensive behavior (for example row scrolling or paging), not globally across the whole screen.
对不属于正常点击/焦点遍历的行为使用按键处理器:
kotlin
Modifier.onPreviewKeyEvent { event ->
if (event.type == KeyEventType.KeyUp && event.key == Key.Back) {
onBack()
true
} else {
false
}
}仅在处理完事件时返回。过于宽泛地返回会破坏文本输入、无障碍快捷方式和父级导航。
truetrue对于快速方向键输入,在拥有昂贵行为的边界处进行节流(例如行滚动或分页),而非在整个屏幕全局节流。
Focus restoration
焦点恢复
Preserve focus by semantic identity:
- Track selected/focused item id, not just index.
- Use stable values in lazy lists and grids.
key - When content refreshes, re-request focus for the same id if it still exists.
- If it no longer exists, choose a deterministic fallback: nearest neighbor, first item, or parent container.
通过语义标识保留焦点:
- 跟踪选中/聚焦项的ID,而非仅索引。
- 在懒加载列表和网格中使用稳定的值。
key - 当内容刷新时,如果相同ID的项仍存在,则重新请求焦点。
- 如果该项不存在,选择确定性的回退方案:最近相邻项、第一个项或父容器。
Common mistakes
常见错误
| Mistake | Fix |
|---|---|
Adding | Add them only when requesting or observing focus |
| Move to |
Initial focus keyed to | Key to loaded/visible condition |
| Focus requesters stored by lazy list index | Store by stable item id |
Everything gets custom | Let spatial search work; override only broken edges |
Key handler returns | Consume only handled keys |
| Tests click nodes in TV/D-pad UI | Send key input and assert focus |
| 错误 | 修复方案 |
|---|---|
给每个按钮都添加 | 仅在需要请求或观察焦点时添加 |
在可组合函数体内调用 | 移至 |
初始焦点与 | 与加载/可见条件关联 |
| 焦点请求器按懒加载列表索引存储 | 按稳定项ID存储 |
所有元素都使用自定义 | 让空间搜索发挥作用;仅覆盖有问题的情况 |
按键处理器对所有按键都返回 | 仅处理已处理的按键 |
| 在电视/方向键UI中测试点击节点 | 发送按键输入并断言焦点 |
Testing
测试
Test focus through user input:
kotlin
composeTestRule.onNodeWithTag("screen").performKeyInput {
pressKey(Key.DirectionDown)
}
composeTestRule.onNodeWithTag("play-button").assertIsFocused()Prefer asserting focused semantics over visual styling. Use screenshot tests only for focus appearance, not for deterministic focus ownership.
Broader test-shape choices (plain UI vs integration, semantics-first): .
compose-ui-testing-patterns通过用户输入测试焦点:
kotlin
composeTestRule.onNodeWithTag("screen").performKeyInput {
pressKey(Key.DirectionDown)
}
composeTestRule.onNodeWithTag("play-button").assertIsFocused()优先断言聚焦语义而非视觉样式。仅将截图测试用于焦点外观,而非确定性焦点归属。
更广泛的测试形态选择(纯UI vs 集成测试、语义优先):。
compose-ui-testing-patternsRed flags during review
评审中的危险信号
- "It focuses correctly when I tap it" for a keyboard/TV UI.
- Initial focus works only with fixed data and fails after loading/refresh.
- Focus state is inferred from selected data state when focus and selection are different concepts.
- The focus graph is described in comments but not encoded or tested.
- 针对键盘/电视UI说“我点击时焦点正常”。
- 初始焦点仅在固定数据下有效,加载/刷新后失效。
- 焦点状态从选中数据状态推断,而焦点与选中是不同概念。
- 焦点图仅在注释中描述,未编码或测试。