mcpField
N
navigation-event

navigation-event

github.com/android開発10.0k605 回インストール2 日前に更新
リポジトリ

navigation-event について

Compose Android アプリで Navigation Event API を実装するためのスキルです。戻るジェスチャー、ディスパッチャー階層、従来の戻るハンドラーからの移行に加え、ライフサイクルや設定上の注意点を扱います。

主な機能

  1. 01Compose Android の設定確認
  2. 02ディスパッチャーの所有権と階層
  3. 03戻るジェスチャーの処理
  4. 04従来の戻るハンドラーからの移行
  5. 05ハンドラーのライフサイクルと解放
  6. 06よくあるエラーの調査

活用シーン

  1. 01BackHandler 実装の移行
  2. 02Compose 画面での戻るジェスチャー処理
  3. 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.md
handle-back.md
index.md
setup.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:計画

このステップを完了するには、次のことを必ず確認してください。

  1. ターゲットプラットフォームの特定:アプリが Compose Android を対象としていることを確認してください。compileSdkが 36 未満の場合は、build.gradle.ktsで36以上に設定してください。
  2. ナビゲーションの確認:Navigation 3 が使用されているか確認してください。使用されている場合は、このスキルで低レベルのディスパッチャーを手動で実装するのではなく、Navigation 3 に組み込まれた戻るナビゲーションのサポートを使用してください。
  3. 階層の確認:戻るジェスチャーのインターセプトや親子ディスパッチャーの連携が必要なホスト Activity、ViewPagers、タブ付きインターフェース、またはネストされたナビゲーションホストを特定してください。
  4. 移行の確認:プロジェクトが戻る操作の処理(OnBackPressedCallback、BackHandler、onBackPresser)からNavigationEventとNavigationBackHandlerに移行中かどうかを確認してください。
  5. 入力のインターセプト: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 に移行されていますか?
  • [ ] プロジェクトは正常にビルドされ、テストに合格していますか?

コメント