1. はじめに
ユーザーはハードウェア キーボードを使用してアプリを操作できます。通常はタブレットや ChromeOS デバイスなどの大画面デバイスで使用しますが、XR デバイスでも利用できます。ユーザーがハードウェア キーボードでもタッチスクリーンと同じように、快適にアプリを操作できるようにすることが重要です。また、タップ入力ではなく D-pad やロータリー エンコーダを使用する可能性のあるテレビや車載ディスプレイ向けにアプリを設計する場合も、同様のキーボード ナビゲーションの原則を適用する必要があります。
Compose を使用すると、ハードウェア キーボード、D-pad、ロータリー エンコーダからの入力を統一された方法で処理できます。これらの入力方法において優れたユーザー エクスペリエンスを実現するための重要な原則は、ユーザーが操作したいインタラクティブなコンポーネントにキーボード フォーカスを直感的かつ一貫して移動できることです。
この Codelab では、以下について学びます。
- 直感的で一貫したナビゲーションを実現するための一般的なキーボード フォーカスの管理パターンを実装する方法
- キーボード フォーカスの移動が正常に動作するかどうかをテストする方法
前提条件
- Compose を使ってアプリを構築した経験
- Kotlin に関する基本的な知識(ラムダやコルーチンなど)
構築内容
次のような一般的なキーボード フォーカスの管理パターンを実装します。
- キーボード フォーカスの移動 - 開始から終了まで、上から下に Z 字型のパターンで移動する
- 論理的な当初のフォーカス - ユーザーが操作する可能性が高い UI 要素にフォーカスを設定する
- フォーカスの復元 - ユーザーが以前操作した UI 要素にフォーカスを移動する
学習内容
- Compose でのフォーカス管理の基本
- UI 要素をフォーカス ターゲットとして作成する方法
- UI 要素にフォーカスを移動するリクエストの方法
- グループ内の特定の UI 要素にキーボード フォーカスを移動する方法
必要なもの
- Android Studio Ladybug 以降のバージョン
- サンプルアプリを実行する次のいずれかのデバイス:
- ハードウェア キーボードを搭載した大画面デバイス
- 大画面デバイス向け Android Virtual Device(サイズ変更可能なエミュレータなど)
2. 設定
- large-screen-codelabs の GitHub リポジトリのクローンを作成します。
git clone https://github.com/android/large-screen-codelabs
または、large-screen-codelabs の ZIP ファイルをダウンロードしてアーカイブを解除します。
focus-management-in-composeフォルダに移動します。- Android Studio でプロジェクトを開きます。
focus-management-in-composeフォルダには 1 つのプロジェクトが含まれています。 - Android タブレット、折りたたみ式デバイス、ハードウェア キーボードを搭載した ChromeOS デバイスがない場合は、Android Studio で [デバイス マネージャー] を開き、[Phone] カテゴリで [Resizable] デバイスを作成します。
図 1. Android Studio でサイズ変更可能なエミュレータを設定します。
3. スターター コードを確認する
このプロジェクトには、次の 2 つのモジュールがあります。
- start - プロジェクトのスターター コードが含まれています。このコードに変更を加えて Codelab を完了します。
- solution - この Codelab の完成したコードが含まれています。
サンプルアプリは次の 3 つのタブで構成されています。
- フォーカス ターゲット
- フォーカス移動順序
- フォーカス グループ
アプリの起動時に、[フォーカス ターゲット] タブが表示されます。
![サンプルアプリの最初のビュー。3 つのタブがあり、[フォーカス ターゲット] タブ(1 つ目)が選択されています。このタブには、列に並べられた 3 つのカードが表示されます。](https://developer.android.com/static/codelabs/large-screens/keyboard-focus-management-in-compose/img/d7f8719233fcdfbb.png?hl=ja)
図 2. アプリの起動時に [フォーカス ターゲット] タブが表示されます。
ui パッケージには、操作する以下の UI コードが含まれています。
App.kt- タブを実装しますtab.FocusTargetTab.kt- [フォーカス ターゲット] タブのコードが含まれますtab.FocusTraversalOrderTab.kt- [フォーカス移動順序] タブのコードが含まれますtab.FocusGroup.kt- [フォーカス グループ] タブのコードが含まれますFocusGroupTabTest.kt-tab.FocusTargetTab.ktのインストルメンテーション テスト(ファイルはandroidTestフォルダにあります)
4. フォーカス ターゲット
フォーカス ターゲットとは、キーボード フォーカスを移動できる UI 要素です。ユーザーは Tab キーと方向(矢印)キーを使用してキーボード フォーカスを移動できます。
Tabキー - フォーカスは次のフォーカス ターゲットまたは前のフォーカス ターゲットに 1 次元で移動します。- 方向キー - フォーカスは 2 次元(上、下、左、右)に移動できます。
タブはフォーカス ターゲットです。サンプルアプリでは、タブがフォーカスを取得すると、タブの背景が視覚的に更新されます。

図 3. フォーカスがフォーカス ターゲットに移動すると、コンポーネントの背景が変わります。
インタラクティブな UI 要素はデフォルトでフォーカス ターゲットになる
インタラクティブなコンポーネントは、デフォルトでフォーカス ターゲットとして設定されます。つまり、ユーザーがタップできる UI 要素はフォーカス ターゲットであるということです。
サンプルアプリの [フォーカス ターゲット] タブには 3 つのカードがあります。1 番目と3 番目のカードはフォーカス ターゲットですが、2 番目のカードはフォーカス ターゲットではありません。ユーザーが Tab キーで 1 番目のカードから 3 番目のカードにフォーカスを移動すると、3 番目のカードの背景が更新されます。
![この GIF アニメーションは、[フォーカス ターゲット] タブでキーボードのフォーカスが最初に移動する様子を示しています。ユーザーが 1 番目のカードで Tab キーを押すと、2 番目のカードをスキップして 3 番目のカードに移動します。](https://developer.android.com/static/codelabs/large-screens/keyboard-focus-management-in-compose/img/57d318d983cf4a70.gif?hl=ja)
図 4. アプリのフォーカス ターゲットには2 番目のカードは含まれません。
2 番目のカードをフォーカス ターゲットに変更する
2 番目のカードをフォーカス ターゲットにするには、インタラクティブな UI 要素に変更します。次のように clickable 修飾子を使用する方法が最も簡単です。
tabsパッケージのFocusTargetTab.ktを開きます。- 次のように、
clickable修飾子を使用してSecondCardコンポーザブルを変更します。
@Composable
fun FocusTargetTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
SecondCard(
modifier = Modifier
.width(240.dp)
.clickable(onClick = onClick)
)
ThirdCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
}
}
実行する
これで、ユーザーは1 番目と3 番目のカードに加えて、2 番目のカードにもフォーカスを移動できるようになります。[フォーカス ターゲット] タブで試すことができます。Tab キーを使用して、1 番目のカードから 2 番目のカードにフォーカスを移動できることを確認します。

図 5. Tab キーを使用して、1 番目のカードから 2 番目のカードにフォーカスを移動します。
5. Z 字型パターンのフォーカス走査
左から右の言語設定では、ユーザーはキーボード フォーカスが左から右、上から下に移動することを想定しています。このフォーカス走査順序は、Z 字型パターンと呼ばれます。
ただし、Compose はレイアウトを無視して、Tab キーの次のフォーカス ターゲットを決定します。代わりにコンポーズ可能な関数の呼び出し順序に基づいて 1 次元のフォーカス走査を使用します。
1 次元のフォーカス走査
1 次元のフォーカス移動順序は、アプリのレイアウトではなく、コンポーズ可能な関数呼び出しの順序に基づいています。
サンプルアプリでは、[フォーカス移動順序] タブでフォーカスが次の順序で移動します。
- 1 番目のカード
- 4 番目のカード
- 3 番目のカード
- 2 番目のカード

図 6. フォーカス走査は、コンポーズ可能な関数の順序に従います。
FocusTraversalOrderTab 関数は、サンプルアプリのフォーカス走査タブを実装しています。この関数は、カードのコンポーズ可能な関数(FirstCard、FourthCard、ThirdCard、SecondCard)をこの順番に呼び出します。
@Composable
fun FocusTraversalOrderTab(
modifier: Modifier = Modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
FourthCard(
onClick = onClick,
modifier = Modifier
.width(240.dp)
.offset(x = 256.dp)
)
ThirdCard(
onClick = onClick,
modifier = Modifier
.width(240.dp)
.offset(y = (-151).dp)
)
}
SecondCard(
modifier = Modifier.width(240.dp)
)
}
}
Z 字型パターンでのフォーカス移動
サンプルアプリの [フォーカス移動順序] タブで、次の手順に沿って Z 字型のフォーカス移動を統合できます。
tabs.FocusTraversalOrderTab.ktを開くThirdCardコンポーザブルとFourthCardコンポーザブルからオフセット修飾子を削除する- タブのレイアウトを、現在の 2 列 1 行から 1 列 2 行に変更する
FirstCardコンポーザブルとSecondCardコンポーザブルを 1 行目に移動するThirdCardコンポーザブルとFourthCardコンポーザブルを 2 行目に移動する
変更後のコードは次のとおりです。
@Composable
fun FocusTraversalOrderTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier
) {
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(240.dp),
)
SecondCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
ThirdCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
FourthCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
}
}
}
実行する
これで、ユーザーは Z 字型パターンでフォーカスを右から左、上から下に移動できるようになります。[フォーカス移動順序] タブで試して、Tab キーでフォーカスが次の順序で移動することを確認できます。
- 1 番目のカード
- 2 番目のカード
- 3 番目のカード
- 4 番目のカード

図 7. Z 字型パターンのフォーカス走査。
6. focusGroup
[フォーカス グループ] タブで right 方向キーを使用すると、フォーカスが 1 番目のカードから 3 番目のカードに移動します。2 つのカードが並んで表示されないため、ユーザーには少しわかりにくいかもしれません。

図 8. 1 番目のカードから 3 番目のカードへの予期しないフォーカス移動。
レイアウト情報に基づく 2 次元フォーカス走査
方向キーを押すと、2 次元のフォーカス走査がトリガーされます。これは、ユーザーが D-padを使用してアプリを操作するときに、テレビでよく見られるフォーカス走査です。キーボードの矢印キーを押すと、D-pad によるナビゲーションを模倣して 2 次元のフォーカス走査もトリガーされます。
2 次元のフォーカス走査では、システムは UI 要素の幾何学的情報を参照し、フォーカスを移動するフォーカス ターゲットを決定します。たとえば、[フォーカス ターゲット] タブからdown 方向キーを使用するとフォーカスが 1 番目のカードに移動し、上方向キーを押すとフォーカスが再び [フォーカス ターゲット] タブに移動します。
![この GIF は、下方向キーでフォーカスが [フォーカス ターゲット] タブから 1 番目のカードに移動し、上方向キーでタブに戻る様子を示しています。これらの 2 つのフォーカス ターゲットは垂直方向で最も近い位置にあります。](https://developer.android.com/static/codelabs/large-screens/keyboard-focus-management-in-compose/img/75a03b55dd08a210.gif?hl=ja)
図 9. 下方向キーと上方向キーによるフォーカス走査。
2 次元フォーカス走査は、Tab キーによる 1 次元フォーカス走査とは異なり、ラップアラウンドしません。たとえば、2 番目のカードにフォーカスが移動したときに、ユーザーは下方向キーでフォーカスを移動できません。

図 10. 2 番目のカードにフォーカスがあるときは、下方向キーではフォーカスを移動できません。
同じレベルにあるフォーカス ターゲット
次のコードで、上記の画面を実装します。フォーカス ターゲットは、FirstCard、SecondCard、ThirdCard、FourthCard の 4 つです。これらの 4 つのフォーカス ターゲットは同じレベルにあり、レイアウト上では ThirdCard は FirstCard の右隣にある最初の項目です。そのため、right 方向キーを使用すると、フォーカスは 1 番目のカードから 3 番目のカードに移動します。
@Composable
fun FocusGroupTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier,
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
) {
SecondCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
ThirdCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
FourthCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
}
}
}
focusGroup 修飾子でフォーカス ターゲットをグループ化する
わかりにくいフォーカスの移動は、次の手順で変更できます。
tabs.FocusGroup.ktを開くFocusGroupTabコンポーズ可能な関数内で、focusGroup修飾子を使用してColumnコンポーズ可能な関数を変更します。
更新後のコードは次のとおりです。
@Composable
fun FocusGroupTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier,
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.focusGroup(),
) {
SecondCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
ThirdCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
FourthCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
}
}
}
focusGroup 修飾子が作成するフォーカス グループに、変更後のコンポーネント内のフォーカス ターゲットが含まれます。フォーカス グループ内のフォーカス ターゲットとフォーカス グループ外のフォーカス ターゲットは異なるレベルにあり、FirstCard コンポーザブルの右側にフォーカス ターゲットが配置されていません。そのため、right 方向キーを使用しても、1 番目のカードから他のカードにフォーカスを移動できません。
実行する
これで、サンプルアプリの [フォーカス グループ] タブでは right 方向キーを押しても、1 番目のカードから 3 番目のカードにフォーカスが移動しなくなりました。
7. フォーカスのリクエスト
ユーザーはキーボードや D-pad を使用して、任意の UI 要素を選択して操作することはできません。ユーザーは、要素を操作する前に、キーボード フォーカスをインタラクティブなコンポーネントに移動する必要があります。
たとえば、カードを操作する前に、フォーカスを [フォーカス ターゲット] タブから 1 番目のカードに移動する必要があります。当初のフォーカスを論理的に設定することで、ユーザーの主なタスクを開始するアクションの数を減らすことができます。

図 11. Tab キーを 3 回押すと、フォーカスが 1 番目のカードに移動します。
FocusRequester でフォーカスをリクエストする
FocusRequester を使用して、UI 要素にフォーカスを移動するようにリクエストできます。requestFocus() メソッドを呼び出す前に、FocusRequester オブジェクトを UI 要素に関連付ける必要があります。
1 番目のカードに当初のフォーカスを設定
当初のフォーカスを 1 番目のカードに設定する手順は次のとおりです。
tabs.FocusTarget.ktを開く- コンポーズ可能な関数
FocusTargetTabでfirstCard値を宣言し、remember関数から返されたFocusRequesterオブジェクトでその値を初期化する focusRequester修飾子を使用してコンポーズ可能な関数FirstCardを変更するfirstCard値をfocusRequester修飾子の引数として指定するUnit値を指定してコンポーズ可能な関数LaunchedEffectを呼び出す。LaunchedEffectコンポーズ可能な関数に渡されたラムダ内でfirstCard値に対して requestFocus() メソッドを呼び出す
FocusRequester オブジェクトが作成され、2 番目と 3 番目のステップで UI 要素に関連付けられる5 番目のステップでは、FocusdTargetTab コンポーザブルが初めてコンポーズされたときに、関連する UI 要素にフォーカスを移動するようリクエストされる
更新されたコードは次のようになります。
@Composable
fun FocusTargetTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
val firstCard = remember { FocusRequester() }
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier
) {
FirstCard(
onClick = onClick,
modifier = Modifier
.width(240.dp)
.focusRequester(focusRequester = firstCard)
)
SecondCard(
modifier = Modifier
.width(240.dp)
.clickable(onClick = onClick)
)
ThirdCard(
onClick = onClick,
modifier = Modifier.width(240.dp)
)
}
LaunchedEffect(Unit) {
firstCard.requestFocus()
}
}
実行する
これで、タブを選択すると、キーボード フォーカスが [フォーカス ターゲット] タブの 1 番目のカードに移動するようになりました。タブを切り替えて試してみましょう。また、アプリの起動時に 1 番目のカードが選択されます。
![この GIF アニメーションは、ユーザーが [フォーカス ターゲット] タブを選択すると、キーボード フォーカスが 1 番目のカードに自動的に移動する様子を示しています。](https://developer.android.com/static/codelabs/large-screens/keyboard-focus-management-in-compose/img/3d84ffcd2e9000bc.gif?hl=ja)
図 12. [フォーカス ターゲット] タブを選択すると、フォーカスが 1 番目のカードに移動します。
8. 選択したタブにフォーカスを移動する
キーボードのフォーカスがフォーカス グループに入るときに、フォーカス ターゲットを指定できます。たとえば、ユーザーがタブ行にフォーカスを移動したとき、選択されたタブにフォーカスを移動できます。
この動作は、次の手順で実装できます。
App.ktを開きます。- コンポーズ可能な関数
AppでfocusRequesters値を宣言します。 FocusRequesterオブジェクトのリストを返すremember関数の戻り値でfocusRequesters値を初期化します。返されるリストの長さは、Screens.entriesの長さと同じにする必要があります。focusRequester修飾子で Tab コンポーザブルを変更して、focusRequester値の各FocusRequesterオブジェクトをTabコンポーザブルに関連付けます。focusProperties修飾子とfocusGroup修飾子で PrimaryTabRow コンポーザブルを変更します。- ラムダを
focusProperties修飾子に渡し、onEnterプロパティを別のラムダに関連付けます。 enterプロパティに関連付けられたラムダから、focusRequesters値でselectedTabIndex値をインデックスとする FocusRequester に対してrequestFocusメソッドを呼び出します。
変更後のコードは次のようになります。
@Composable
fun App(
modifier: Modifier = Modifier,
) {
val context = LocalContext.current
val backstack = rememberNavBackStack(Tab.FocusTarget)
val selectedTabIndex = Tab.entries.indexOf(backstack.last())
val focusRequesters = remember {
List(Tab.entries.size) { FocusRequester() }
}
Column(modifier = modifier) {
PrimaryTabRow(
selectedTabIndex = selectedTabIndex,
modifier = Modifier
.focusProperties {
onEnter = {
focusRequesters[selectedTabIndex].requestFocus()
}
}
.focusGroup()
) {
Tab.entries.forEachIndexed { index, tab ->
val isSelected = selectedTabIndex == index
Tab(
selected = isSelected,
onClick = {
if (!isSelected) {
backstack.add(tab)
}
},
text = { Text(stringResource(tab.title)) },
modifier = Modifier.focusRequester(focusRequester = focusRequesters[index])
)
}
}
NavDisplay(
backStack = backstack,
entryProvider = entryProvider {
entry<Tab.FocusTarget> {
FocusTargetTab(
onClick = context::onCardClicked,
modifier = Modifier.padding(32.dp),
)
}
entry<Tab.FocusTraversalOrder> {
FocusTraversalOrderTab(
onClick = context::onCardClicked,
modifier = Modifier.padding(32.dp)
)
}
entry<Tab.FocusGroup> {
FocusGroupTab(
onClick = context::onCardClicked,
modifier = Modifier.padding(32.dp)
)
}
}
)
}
}
フォーカスの移動は、focusProperties 修飾子で制御できます。修飾子に渡されるラムダで、FocusProperties を変更します。変更された UI 要素にフォーカスがある状態で、ユーザーが Tab キーまたは方向キーを押すと、システムはフォーカス ターゲットを選択するために FocusProperties を参照します。
フォーカスがフォーカス グループに入ると、onEnter プロパティに設定されたラムダが呼び出されます。ラムダでフォーカスをリクエストすることで、UI 要素にフォーカスを移動できます。
実行する
これで、ユーザーがタブ行にフォーカスを移動すると、キーボード フォーカスが選択したタブに移動するようになりました。次の手順で試すことができます。
- アプリを実行する
- [フォーカス グループ] タブを選択する
down方向キーを使用して、1 番目のカードにフォーカスを移動するup方向キーでフォーカスを移動する
図 13. 選択したタブにフォーカスが移動します。
9. フォーカスの復元
ユーザーは、タスクが中断されたときに簡単に再開できることを求めています。フォーカスの復元は、中断からの再開に対応しています。フォーカスの復元では、以前に選択された UI 要素にキーボード フォーカスが移動します。
フォーカスの復元の一般的なユースケースとして、動画ストリーミング アプリのホーム画面があります。画面には、カテゴリ内のムービーやテレビ番組のエピソードなど、動画コンテンツの複数のリストが表示されます。ユーザーはリストを見て、興味のあるコンテンツを探します。以前に確認したリストに戻って引き続き閲覧することもあります。フォーカスの復元を使用すると、ユーザーはリストで最後に見たアイテムにキーボード フォーカスを移動せずに、引き続き閲覧できます。
focusRestorer 修飾子でフォーカスをフォーカス グループに復元
focusRestorer 修飾子を使用して、フォーカス グループにフォーカスを保存して復元します。フォーカスがフォーカス グループから離れると、フォーカスは以前にフォーカスされていたアイテムの参照を保存します。その後、フォーカスがフォーカス グループに戻ると、以前フォーカスされていたアイテムにフォーカスが復元されます。
フォーカスの復元をフォーカス グループタブに統合
サンプルアプリの [フォーカス グループ] タブには、2 番目のカード、3 番目のカード、4 番目のカードを含む行があります。
図 14. 2 番目のカード、3 番目のカード、4 番目のカードを含むフォーカス グループ。
行でフォーカスの復元を統合する手順は次のとおりです。
tab.FocusGroupTab.ktを開くfocusRestorer修飾子で、FocusGroupTabコンポーザブル内のRowコンポーザブルを変更します。この修飾子は、focusGroup修飾子の前に呼び出す必要があります。
変更後のコードは次のようになります。
@Composable
fun FocusGroupTab(
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = modifier,
) {
FirstCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
Row(
horizontalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.focusRestorer()
.focusGroup(),
) {
SecondCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
ThirdCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
FourthCard(
onClick = onClick,
modifier = Modifier.width(208.dp)
)
}
}
}
実行する
これで、[フォーカス グループ] タブの行にフォーカスが戻ります。次の手順で試すことができます。
- [フォーカス グループ] タブを選択する
- 1 番目のカードにフォーカスを移動する
Tabキーを押して 4 番目のカードにフォーカスを移動するup方向キーを使用して 1 番目のカードにフォーカスを移動するTabキーを押す
キーボード フォーカスは 4 番目のカードに移動します。これは、focusRestorer 修飾子がカードの参照を保存し、キーボード フォーカスが行に設定されたフォーカス グループに入るとフォーカスを復元するためです。

図 15. 上方向キーを押してから Tab キーを押すと、フォーカスが 4 番目のカードに戻ります。
10. テストの作成
実装したキーボード フォーカス管理は、テストで確認できます。Compose には、UI 要素がフォーカスされているかどうかをテストし、UI コンポーネントでキー操作を行うための API が用意されています。詳しくは、Jetpack Compose でのテストの Codelab をご覧ください。
フォーカス ターゲットタブのテスト
前のセクションでは、2 番目のカードをフォーカス ターゲットとして設定するようにコンポーズ可能な関数 FocusTargetTab を変更しました。前のセクションで手動で行った実装のテストを作成します。テストを作成する手順は次のとおりです。
FocusTargetTabTest.ktを開きます。次の手順でtestSecondCardIsFocusTarget関数を変更します。- 1 番目のカードの
SemanticsNodeInteractionオブジェクトでrequestFocusメソッドを呼び出し、フォーカスを 1 番目のカードに移動するようリクエストします。 assertIsFocused()メソッドで 1 番目のカードがフォーカスされていることを確認します。performKeyInputメソッドに渡されたラムダ内のKey.Tab値を使用してpressKeyメソッドを呼び出し、Tabキー操作を実行します。- 2 番目のカードの
SemanticsNodeInteractionオブジェクトでassertIsFocused()メソッドを呼び出し、キーボード フォーカスが 2 番目のカードに移動するかどうかをテストします。
更新されたコードは次のようになります。
@OptIn(ExperimentalTestApi::class, ExperimentalComposeUiApi::class)
@Test
fun testSecondCardIsFocusTarget() {
composeTestRule.setContent {
LocalInputModeManager
.current
.requestInputMode(InputMode.Keyboard)
FocusTargetTab(onClick = {})
}
val context = InstrumentationRegistry.getInstrumentation().targetContext
// Ensure the 1st card is focused
composeTestRule
.onNodeWithText(context.getString(R.string.first_card))
.requestFocus()
.performKeyInput { pressKey(Key.Tab) }
// Test if focus moves to the 2nd card from the 1st card with Tab key
composeTestRule
.onNodeWithText(context.getString(R.string.second_card))
.assertIsFocused()
}
実行する
テストを実行するには、FocusTargetTest クラス宣言の左側に表示されている三角形のアイコンをクリックします。詳しくは、Android Studio でのテストのテストの実行をご覧ください。

11. 完了
これで、キーボードのフォーカス管理の構成要素について学習できました。
- フォーカス ターゲット
- フォーカス走査
フォーカス移動順序は、次の Compose 修飾子で制御できます。
focusGroup修飾子focusProperties修飾子
ハードウェア キーボード、当初のフォーカス、フォーカスの復元を使用した UX の一般的なパターンを実装しました。これらのパターンは、次の API を組み合わせて実装できます。
FocusRequesterクラスfocusRequester修飾子focusRestorer修飾子LaunchedEffectコンポーズ可能な関数
実装された UX は、インストルメンテーション テストで確認できます。Compose には、キー操作を実行し、SemanticsNode にキーボード フォーカスがあるかどうかをテストする方法が用意されています。
