compose-focus-navigation について
compose-focus-navigationは、キーボード、D-pad、リモコンによるフォーカス移動を設定・テストするJetpack Compose開発者向けのスキルです。例外的な移動経路を明示し、ユーザーと同じ入力方法でフォーカス状態を検証します。
主な機能
- 01必要に応じてフォーカス要求や状態監視を追加
- 02既定の空間検索が不適切な場合だけ経路を指定
- 03独自動作が必要なキーだけを処理
- 04更新後は意味上の識別子でフォーカスを復元
- 05キー入力とフォーカスセマンティクスでテスト
活用シーン
- 01初期フォーカスや復元フォーカスの設定
- 02特定方向へのフォーカス移動を修正
- 03リスト更新後の安定したフォーカス先の選択
- 04キーボードやD-padの移動動作を検証
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ chrisbanes/ skills --skill compose-focus-navigationインストール後、Claude Code で/compose-focus-navigationを入力して読み込まれたか確認します。
詳細
Compose:フォーカスナビゲーション
基本原則
フォーカスは状態を持つ UI の動作です。対象と例外的な移動先を明示し、ユーザーのキーボード、D-pad、またはリモート入力を通じて操作し、検証します。
手順
- まず、すでにフォーカスに対応しているコンポーネントを使います。フックを追加するのは、要求された動作に限ります。
| 必要なこと | 追加するもの |
|---|---|
| 通常のボタン/テキストフィールド/クリック可能要素のフォーカス | 追加なし。フォーカス可能なコンポーネントを使います |
| プログラムによる初期フォーカス/復元フォーカス | FocusRequester + Modifier.focusRequester(...) |
| フォーカス変更に対する視覚的な反応または状態変化 | Modifier.onFocusChanged { ... } |
| まだフォーカス可能ではない独自のインタラクティブな領域 | Modifier.focusable() と、必要に応じたロール/セマンティクス |
- 対象が表示される条件をキーとして、
LaunchedEffectから初期フォーカスまたは復元フォーカスを要求します。遅延読み込みコンテンツでは、安定したアイテム ID ごとにリクエスターを保持し、アイテムがコンポーズされた後にのみ要求します。AnimatedContent内では、描画対象の識別、タグ、リクエスターの所有者、エフェクトのキーに、コンテンツラムダのターゲットを一貫して使用します。外側の状態をキャプチャすると、退出するコンテンツと入場するコンテンツに同じ識別情報が使われます。 レビューでは、requestFocus()がコンポーザブル本体で実行されているという指摘だけでは不十分です。対象が利用可能になった後に要求を発行するイベント、またはキー付きエフェクトも明示してください。 - 具体的な移動先、ジャンプ、トラップに問題がない限り、デフォルトの空間検索を維持します。
focusPropertiesで指定するのは、そうした例外だけにします。 - 通常のクリックやフォーカス移動では実現できない動作に限り、キーを処理します。処理したイベントだけを消費し、D-pad による高負荷な処理の連続実行は、画面全体ではなく負荷のかかる処理の所有者側で抑制します。
- 更新後は意味上の識別情報に基づいてフォーカスを復元します。フォーカス中の ID が存在する場合は保持し、存在しない場合は決定的なフォールバックを選びます。
- 具体的なキー/D-pad 操作と、フォーカス済みのセマンティクスを使ってテストします。レビュー対象の動作がユーザー操作によって起こる場合、直接の状態変更で代用したり、入力を条件付きのままにしたりしないでください。スクリーンショットを使うのはフォーカスの外観だけです。
- 意図した対象と例外的な移動先がすべて定義され、読み込み/更新時のフォーカスポリシーが安定し、テストでユーザーと同じ入力モデルを使っていれば完了です。
たとえば、どちらの動作も必要な場合に限り、フォーカスを要求して状態を監視します。
val requester = remember { FocusRequester() }
Button(
onClick = onClick,
modifier = Modifier
.focusRequester(requester)
.onFocusChanged { state -> isFocused = state.isFocused },
) {
Text("Play")
}
コンポーザブル本体ではなく、エフェクトからフォーカスを要求します。
val initialFocus = remember { FocusRequester() }
LaunchedEffect(initialFocus) {
initialFocus.requestFocus()
}
読み込み後に対象が表示される場合は、その条件をキーにして要求します。
LaunchedEffect(items.isNotEmpty()) {
if (items.isNotEmpty()) {
firstItemRequester.requestFocus()
}
}
デフォルトの空間検索に問題がある場合に限り、focusProperties を使います。
Modifier.focusProperties {
up = headerRequester
down = firstRowRequester
left = FocusRequester.Cancel
}
ハードコードされたリンクが多すぎると、フォーカスグラフが古くなります。特別なキー動作では、処理したイベントだけを消費します。
Modifier.onPreviewKeyEvent { event ->
if (event.type == KeyEventType.KeyUp && event.key == Key.Back) {
onBack()
true
} else {
false
}
}
ユーザー入力を通じてフォーカスをテストします。
composeTestRule.onNodeWithTag("screen").performKeyInput {
pressKey(Key.DirectionDown)
}
composeTestRule.onNodeWithTag("play-button").assertIsFocused()
テストの構成に関するより広範な選択肢については、Compose UI testing patterns を参照してください。
コメント
Markdown 対応