navigation-event について
Compose Android アプリで Navigation Event API を実装するためのスキルです。戻るジェスチャー、ディスパッチャー階層、従来の戻るハンドラーからの移行に加え、ライフサイクルや設定上の注意点を扱います。
主な機能
- 01Compose Android の設定確認
- 02ディスパッチャーの所有権と階層
- 03戻るジェスチャーの処理
- 04従来の戻るハンドラーからの移行
- 05ハンドラーのライフサイクルと解放
- 06よくあるエラーの調査
活用シーン
- 01BackHandler 実装の移行
- 02Compose 画面での戻るジェスチャー処理
- 03タブや入れ子のホストでのナビゲーションスコープ設定
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ android/ skills --skill navigation-eventインストール後、Claude Code で/navigation-eventを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/android/guide/navigation/navigation-event/dispatcher.mdhandle-back.mdindex.mdsetup.md詳細
共通ガイドライン
- アーキテクチャの概念について:基盤となるアーキテクチャ、継続的なジェスチャーイベントのライフサイクル、または Navigation Event ライブラリのクラス定義を理解するには、Navigation Event overviewをお読みください。
- Android のターゲットについて:コンパイル SDK が 36 未満の場合は、
build.gradle.ktsで36以上に設定してください。 - Compose の Android ターゲットについて:Compose 固有の API を使用するには、プロジェクトで Jetpack Compose を使用する必要があります。このスキルの対象は Compose Android のみです(Android Views と Compose 以外の実装は対象外です)。
- Activity のディスパッチャーについて:
ComponentActivityは標準でNavigationEventDispatcherOwnerを自動的に実装します。匿名のデリゲートオーナーを作成したり、メンバープロパティをオーバーライドしたりせず、組み込みのnavigationEventDispatcherを使用してください。 - ダイアログのスコープ設定について:フローティングウィンドウ(Compose の
Dialog、ModalBottomSheet、ComponentDialog)は、NavigationEventDispatcherOwnerを自動的に提供します。ダイアログに対して手動でCompositionLocalProviderを伝播させる必要はありません。 - 親子ディスパッチャー階層について:Compose で
ViewPagers、タブ付きインターフェース、またはネストされたナビゲーションコンテナーにナビゲーション処理のスコープを設定する場合は、rememberNavigationEventDispatcherOwner()を使用して、親に紐付いた子オーナーを作成してください。オーナーを無効化すると(enabled = false)、すべての子ハンドラーも自動的に無効化されます。 - Compose のハンドラーについて:
NavigationEventStateとハンドラーの一対一の関係が厳密に適用されます。同じNavigationEventStateを複数のアクティブなNavigationBackHandlerインスタンスにバインドしないでください(IllegalArgumentException)。
ステップ 1:計画
このステップを完了するには、次のことを必ず確認してください。
- ターゲットプラットフォームの特定:アプリが Compose Android を対象としていることを確認してください。
compileSdkが 36 未満の場合は、build.gradle.ktsで36以上に設定してください。 - ナビゲーションの確認:Navigation 3 が使用されているか確認してください。使用されている場合は、このスキルで低レベルのディスパッチャーを手動で実装するのではなく、Navigation 3 に組み込まれた戻るナビゲーションのサポートを使用してください。
- 階層の確認:戻るジェスチャーのインターセプトや親子ディスパッチャーの連携が必要なホスト Activity、
ViewPagers、タブ付きインターフェース、またはネストされたナビゲーションホストを特定してください。 - 移行の確認:プロジェクトが戻る操作の処理(
OnBackPressedCallback、BackHandler、onBackPresser)からNavigationEventとNavigationBackHandlerに移行中かどうかを確認してください。 - 入力のインターセプト:
NavigationEventへの変換が必要な、ジェスチャーまたはハードウェアボタンの押下によるナビゲーションイベントをアプリがインターセプトしている箇所を特定してください。
ステップ 2:依存関係のセットアップ
このステップを完了するには、次のことを必ず確認してください。
- コンパイル SDK のセットアップ、カタログバージョンの宣言、依存関係の追加については、セットアップガイドに従ってください。
ステップ 3:ディスパッチャーと入力の設定
このステップを完了するには、次のことを必ず確認してください。
- ディスパッチャーの設定には、
ComponentActivityまたはComponentDialogのオーナー解決の自動化を活用してください。 - ディスパッチャーガイドに従って、Compose で親子ディスパッチャーを連携させてください。
ステップ 4:戻るナビゲーションと UI トランジションの処理
このステップを完了するには、次のことを必ず確認してください。
- ナビゲーションイベントハンドラーの作成、Compose での戻るジェスチャーのインターセプト、スワイプ中の UI コンポーネントのアニメーション、および従来の戻るハンドラーからの移行については、戻る操作の処理ガイドに従ってください。
ステップ 5:リソースをクリーンアップする
警告
警告: Compose API はティアダウンを自動的に実行します。
NavigationBackHandlerやrememberNavigationEventDispatcherOwner()などの Compose API を使用している場合、コンポーザブルがコンポジションから離れると、ハンドラーの削除とディスパッチャーの破棄が自動的に行われます。
カスタムディスパッチャーまたは Compose 以外のハンドラーを管理する場合に限り、必ず手動で明示的なクリーンアップを行ってください。
- ティアダウン時に、アクティブなハンドラーに対して
remove()を呼び出します。 - ナビゲーションのサブツリーを一時的に無効にするには、
isEnabled = falseを呼び出します。 - ホストコンポーネントの破棄時に、ディスパッチャーインスタンスの
dispose()を呼び出します。親ディスパッチャーを破棄すると、すべての子ディスパッチャーにも自動的に処理が適用されます。
トラブルシューティングの基本ガイドライン
1. Activity ディスパッチャーのセットアップ(StackOverflowError の再帰)
ComponentActivity は、標準で NavigationEventDispatcherOwner を自動的に実装しています。navigationEventDispatcher をオーバーライドしたり、匿名のデリゲートオーナーでラップしたりしないでください。
正しい例
これが正しい理由:Compose アプリでは、ホストとして ComponentActivity を使用します。
LocalNavigationEventDispatcherOwner.current は、Activity に組み込まれているディスパッチャーを自動的に解決します。
// RIGHT
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationContent()
}
}
}
誤った例
これが誤っている理由:MainActivity に NavigationEventDispatcherOwner を直接実装し、navigationEventDispatcher を新しいインスタンスでオーバーライドすると、ライブラリの拡張プロパティが隠され、起動時に再帰的な無限ループによるクラッシュ(StackOverflowError)が発生します。冗長な匿名デリゲートオーナー(object : NavigationEventDispatcherOwner)を作成する必要はありません。
// WRONG
class MainActivity : ComponentActivity(), NavigationEventDispatcherOwner {
override val navigationEventDispatcher = NavigationEventDispatcher() // Shadow loop crash
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApplicationContent()
}
}
}
2. フローティングウィンドウとダイアログのスコープ設定(ComponentDialog オーナーの自動設定)
フローティングウィンドウ(Compose の Dialog、ModalBottomSheet、および ComponentDialog を基盤とするすべてのウィンドウ)では、NavigationEventDispatcherOwner が自動的に提供されます。ダイアログ内で CompositionLocalProvider を使用して LocalNavigationEventDispatcherOwner を手動で再提供しないでください。
正しい例
これが正しい理由:ComponentDialog はナビゲーションディスパッチャーを自動的に処理します。Compose の Dialog コンポーネントでは、手動で伝播しなくても、標準でディスパッチャーオーナーが解決されます。
// RIGHT
@Composable
fun MyDialog(onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
onBackCompleted = onDismiss
)
}
}
誤った例
これが誤っている理由:ダイアログのコンテンツを手動で CompositionLocalProvider で囲むと、冗長なボイラープレートが生じ、ComponentDialog が提供する自動ディスパッチャー解決の仕組みが分かりにくくなります。
// WRONG
@Composable
fun MyDialog(onDismiss: () -> Unit) {
val dispatcherOwner = LocalNavigationEventDispatcherOwner.current!!
Dialog(onDismissRequest = onDismiss) {
// Redundant: ComponentDialog provides NavigationEventDispatcherOwner automatically
CompositionLocalProvider( LocalNavigationEventDispatcherOwner provides dispatcherOwner) {
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
onBackCompleted = onDismiss
)
}
}
}
3. 親子ディスパッチャーの階層(ViewPagers とネストされたナビゲーション)
ViewPagers、タブ付きインターフェース、Compose のカスタムナビゲーションコンテナなど、ネストされた UI 階層を管理する場合は、rememberNavigationEventDispatcherOwner() を使用して、コンポジション階層にリンクされた子オーナーを作成してください。子オーナーで enabled = false を設定すると、そのディスパッチャーと登録済みのすべての子ハンドラーが自動的に無効になります。
正しい例
これが正しい理由:rememberNavigationEventDispatcherOwner(enabled = isSelected) を使用すると、LocalNavigationEventDispatcherOwner.current の親にリンクされたスコープ付きの子ディスパッチャーが作成されます。CompositionLocalProvider を使用してこれを提供すると、表示されていないタブやページがバックジェスチャーを自動的にインターセプトしなくなり、ハンドラーのリークも防げます。
// RIGHT: Scoping child navigation in a ViewPager or Tab interface
@Composable
fun TabPage(isSelected: Boolean) {
val childOwner = rememberNavigationEventDispatcherOwner(enabled = isSelected)
CompositionLocalProvider(LocalNavigationEventDispatcherOwner provides childOwner) {
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
onBackCompleted = { /* Handle page back navigation */ }
)
// Page content
}
}
誤った例
これが誤っている理由:リンクされていない単独のディスパッチャーを作成すること、再コンポジション間で状態を保持せずに生のディスパッチャーをインスタンス化すること、または .addChild() のような存在しないメソッドを使おうとすることは、階層ルーティングを壊し、ページが非アクティブな場合でも子ハンドラーをアクティブなままにします。
// WRONG
@Composable
fun TabPage(isSelected: Boolean) {
val parentDispatcher = LocalNavigationEventDispatcherOwner.current?.navigationEventDispatcher
val childDispatcher = NavigationEventDispatcher() // Unlinked and not remembered across recompositions
// WRONG: Method does not exist
parentDispatcher?.addChild(childDispatcher)
}
4. Compose での複数ハンドラー登録(IllegalArgumentException)
同じ NavigationEventState を複数のアクティブな NavigationBackHandler インスタンスにバインドしてはいけません。実行時に IllegalArgumentException がスローされます。未保存の変更を確認する場合と、すぐに戻る場合のような条件付きワークフローを処理するには、ハンドラーを 1 つだけ登録し、onBackCompleted 内でロジックを分岐させてください。
正しい例
これが正しい理由:onBackCompleted 内で分岐ロジックを処理する単一の NavigationBackHandler を使用することで、NavigationEventState とハンドラーの厳密な 1 対 1 の対応関係が保たれ、状態の衝突を防げます。
// RIGHT
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
isBackEnabled = true,
onBackCompleted = {
if (hasUnsavedChanges) {
showDiscardDialog()
} else {
onNavigateUp()
}
}
)
誤った例
これが誤りである理由:同じ navigationState インスタンスに複数の NavigationBackHandler コンポーザブルをアタッチすると、単一の状態オブジェクトに重複するハンドラーをバインドしようとするため、実行時に IllegalArgumentException がスローされます。
// WRONG
val navigationState = rememberNavigationEventState(currentInfo = NavigationEventInfo.None)
NavigationBackHandler(
state = navigationState,
isBackEnabled = hasUnsavedChanges,
onBackCompleted = { /* Discard changes */ }
)
NavigationBackHandler(
state = navigationState,
isBackEnabled = !hasUnsavedChanges,
onBackCompleted = { /* Navigate up */ }
)
チェックリスト
Compose Android ターゲットの場合:
- [ ] コンパイル SDK は
36以上に設定されていますか?(コンパイル SDK が 36 未満の場合は、build.gradle.ktsで36以上に設定してください)。 - [ ]
AndroidManifest.xmlでandroid:enableOnBackInvokedCallbackが"false"に明示的に設定されていませんか?(API 36 以上ではデフォルトで"true"です。API 33~35 では"true"に設定されていることを確認してください)。 - [ ] Activity は、冗長な匿名デリゲートによるラップをせず、組み込みの
ComponentActivityディスパッチャーオーナーを使用していますか? - [ ] ダイアログやシートは、冗長な
CompositionLocalProviderによるラップをせず、自動的なComponentDialogディスパッチャー解決を使用していますか? - [ ] ネストされた階層を管理する際、Compose の親子ディスパッチャー関係は
rememberNavigationEventDispatcherOwner()を使ってスコープ化されていますか? - [ ] 重複登録(
IllegalArgumentException)を避けるため、条件付きの戻る操作ロジックは単一の統合されたNavigationBackHandler内で処理されていますか? - [ ] 従来の
BackHandlerの使用箇所は、予測型の進捗に対応したNavigationBackHandlerに移行されていますか? - [ ] プロジェクトは正常にビルドされ、テストに合格していますか?