Jetpack Navigation 3とは? Jetpack Navigation 3 は、以前のバージョンとは根本的に異なる新しい Google ナビゲーション ライブラリです。 — 通常の変数リストで、各要素がアプリケーションの画面を表します。 NavBackStack あなたはこのリストから要素を追加して削除し、UI が自動的に更新されます。 普通のKOTLINクラス。 NavKey これにより、ナビゲーションを完全にコントロールできますが、典型的な操作にはかなり多くのボイラープレートコードを書く必要があります。 なぜNavBackStackと直接作業するのは不便なのか コードがどのように見えるかを見てみましょう。 : NavBackStack @Composable fun MyApp() { val backStack = rememberNavBackStack(Screen.Home) // Add a screen backStack.add(Screen.Details("123")) // Go back backStack.removeLastOrNull() // Replace current screen backStack.set(backStack.lastIndex, Screen.Success) } トラブルは、ViewModel からナビゲーションを起動する必要があります。 ViewModel (ViewModel が Compose 特有のものについて知るべきではないと考えているように、私の理解では建築原則に違反している) や、各ナビゲーションアクションに対して中間のコールバックを作成します。 NavBackStack さらに、直接ステックで作業する際には、エッジケースの処理を忘れるのが簡単です。 Nav3 Routerが作業を簡素化する方法 Nav3 Router は、ナビゲーション 3 に対して、ナビゲーションのための既知の API を提供する薄い包装です. インデックスやリスト操作について考える代わりに、あなたは単に「スクリーン X に移動する」または「戻る」と言います。 重要なポイント: Nav3 Router は独自のスタックを作成しません。 ナビゲーション3が提供するもので、作業をより便利にします。 , the library translates this into the corresponding operation with the original stack. 図書館は、これをオリジナルスタックと関連する操作に翻訳します。 NavBackStack router.push(Screen.Details) 主な利点: ViewModel から使用できます。 ナビゲーションコマンドは、UIが一時的に利用できない場合(たとえば、スクリーン回転中に)バッファされます。 すべてのステック操作は原子的に行われます。 明るい火 変更とカスタム行動の追加における柔軟性 設置 Nav3 Router は Maven Central でご利用いただけます。 : build.gradle.kts // For shared module in KMP project kotlin { sourceSets { commonMain.dependencies { implementation("io.github.arttttt.nav3router:nav3router:1.0.0") } } } // For Android-only project dependencies { implementation("io.github.arttttt.nav3router:nav3router:1.0.0") } 図書館のソースコードはGitHubで利用できます。 github.com/arttttt/Nav3ルーター Nav3 Routerの構造 図書館は3つの主要な部分で構成され、それぞれが独自の課題を解決する。 Router - 開発者インターフェイス Router は、そのような方法を提供します。 で、 で、 あなたがこれらの方法を呼び出すとき、ルーターは相応のコマンドを作成し、それらを連鎖に送信します。ルーター自体は、ナビゲーションがどのように実行されるかを知りません - これはどこからでも使用することができます。 push() pop() replace() CommandQueue - コマンドとその実行の間のバッファ CommandQueue はタイミングの問題を解決します。想像してください:ユーザーは画面の回転中にボタンを押しました. UI が再生され、ナビゲーターが一時的に利用できなくなっています. CommandQueue はコマンドを保存し、ナビゲーターが再起動すると実行します. これがなければ、コマンドは単に失われます。 // Simplified queue logic class CommandQueue<T : Any> { private var navigator: Navigator<T>? = null private val pending = mutableListOf<Command<T>>() fun executeCommand(command: Command<T>) { if (navigator != null) { navigator.apply(command) // Navigator exists - execute immediately } else { pending.add(command) // No - save for later } } } Navigator - The One Who Works with the Stack(ナビゲーター) Navigator はコマンドを取得し、それらを適用します。 重要な詳細:最初に現在のスタックのコピーを作成し、それにすべてのコマンドを適用し、次に原子的にオリジナルスタックを変更されたコピーに置き換える。 NavBackStack // Simplified Navigator logic fun applyCommands(commands: Array<Command>) { val stackCopy = backStack.toMutableList() // Work with a copy for (command in commands) { when (command) { is Push -> stackCopy.add(command.screen) is Pop -> stackCopy.removeLastOrNull() // ... other commands } } backStack.swap(stackCopy) // Atomically apply changes } Nav3 Routerを起動する 最も簡単な方法は、ルーターを手動で作成さえしないことです Nav3Host はあなたのためにそれを行うでしょう: @Composable fun App() { val backStack = rememberNavBackStack(Screen.Home) // Nav3Host will create Router automatically Nav3Host(backStack = backStack) { backStack, onBack, router -> NavDisplay( backStack = backStack, onBack = onBack, entryProvider = entryProvider { entry<Screen.Home> { HomeScreen( onOpenDetails = { router.push(Screen.Details) // Use router } ) } entry<Screen.Details> { DetailsScreen( onBack = { router.pop() } ) } } ) } } より複雑なアプリケーションでは、DIを通じてルーターを作成し、ViewModelに送信することに意味があります。 スクリーンの定義 @Serializable sealed interface Screen : NavKey { @Serializable data object Home : Screen @Serializable data class Product(val id: String) : Screen @Serializable data object Cart : Screen } ルーターを Nav3Host に送信します。 @Composable fun App() { val backStack = rememberNavBackStack(Screen.Home) val router: Router<Screen> = getSomehowUsingDI() // Pass Router to Nav3Host Nav3Host( backStack = backStack, router = router, ) { backStack, onBack, _ -> NavDisplay( backStack = backStack, onBack = onBack, entryProvider = entryProvider { entry<Screen.Home> { HomeScreen() } entry<Screen.Details> { DetailsScreen() } } ) } } ViewModel は Constructor 経由で Router を受け取ります。 class ProductViewModel( private val router: Router<Screen>, private val cartRepository: CartRepository ) : ViewModel() { fun addToCart(productId: String) { viewModelScope.launch { cartRepository.add(productId) router.push(Screen.Cart) // Navigation from ViewModel } } } UIでは、ViewModelを使います。 @Composable fun ProductScreen(viewModel: ProductViewModel = koinViewModel()) { Button(onClick = { viewModel.addToCart(productId) }) { Text("Add to Cart") } } 典型的なシナリオの例 シンプルな前後ナビゲーション // Navigate to a new screen router.push(Screen.Details(productId)) // Go back router.pop() // Navigate with replacement of current screen (can't go back) router.replaceCurrent(Screen.Success) スクリーンチェーンで働く // Open multiple screens at once router.push( Screen.Category("electronics"), Screen.Product("laptop-123"), Screen.Reviews("laptop-123") ) // Return to a specific screen // Will remove all screens above Product from the stack router.popTo(Screen.Product("laptop-123")) チェックインシナリオ @Composable fun CheckoutScreen(router: Router<Screen>) { Button( onClick = { // After checkout we need to: // 1. Show confirmation screen // 2. Prevent going back to cart router.replaceStack( Screen.Home, Screen.OrderSuccess(orderId) ) // Now only Home and OrderSuccess are in the stack } ) { Text("Place Order") } } EXIT NESTED NAVIGATION // User is deep in settings: // Home -> Settings -> Account -> Privacy -> DataManagement // "Done" button should return to home Button( onClick = { // Will leave only root (Home) router.clearStack() } ) { Text("Done") } // Or if you need to close the app from anywhere Button( onClick = { // Will leave only current screen and trigger system back router.dropStack() } ) { Text("Exit") } ボーナス: SceneStrategy and Dialogs これまで、画面間のシンプルなナビゲーションについてだけ話し合ったことがあります。しかし、ダイアログや底表を表示する必要がある場合はどうでしょうか? Navigation 3 の SceneStrategy コンセプトが役立ちます。 舞台戦略とは? SceneStrategy は、スタックからスクリーンがどのように表示されるかを正確に決定するメカニズムです。 ステックから最後の画面を表示するだけですが、より複雑なシナリオのための独自の戦略を作成できます。 SinglePaneSceneStrategy SceneStrategyを、あなたのスクリーンスタックを見て「OK、これらの3つのスクリーンは正常に表示されますが、この最後の1つは以前のものの上にモダルウィンドウとして表示されます」と判断するディレクターとして考えてください。 ModalBottomSheetのための戦略の作成 まず、そのような画面をどのようにマークするかを定義しましょう: @Serializable sealed interface Screen : NavKey { @Serializable data object Home : Screen @Serializable data class Product(val id: String) : Screen // This screen will be shown as bottom sheet @Serializable data object Filters : Screen } 最後の画面のメタデータをチェックし、特別なマーカーを見つけたら、底表として表示します。 class BottomSheetSceneStrategy<T : Any> : SceneStrategy<T> { companion object { // Metadata key by which we identify bottom sheet private const val BOTTOM_SHEET_KEY = "bottomsheet" // Helper function to create metadata fun bottomSheet(): Map<String, Any> { return mapOf(BOTTOM_SHEET_KEY to true) } } @Composable override fun calculateScene( entries: List<NavEntry<T>>, onBack: (Int) -> Unit ): Scene<T>? { val lastEntry = entries.lastOrNull() ?: return null // Check if the last screen has bottom sheet marker val isBottomSheet = lastEntry.metadata[BOTTOM_SHEET_KEY] as? Boolean if (isBottomSheet == true) { // Return special Scene for bottom sheet return BottomSheetScene( entry = lastEntry, previousEntries = entries.dropLast(1), onBack = onBack ) } // This is not a bottom sheet, let another strategy handle it return null } } 複数の戦略を組み合わせる 実際のアプリケーションでは、底表、ダイアログ、および通常のスクリーンが必要かもしれません. For this, you can create a delegate strategy that will choose the right strategy for each screen: class DelegatedScreenStrategy<T : Any>( private val strategyMap: Map<String, SceneStrategy<T>>, private val fallbackStrategy: SceneStrategy<T> ) : SceneStrategy<T> { @Composable override fun calculateScene( entries: List<NavEntry<T>>, onBack: (Int) -> Unit ): Scene<T>? { val lastEntry = entries.lastOrNull() ?: return null // Check all keys in metadata for (key in lastEntry.metadata.keys) { val strategy = strategyMap[key] if (strategy != null) { // Found suitable strategy return strategy.calculateScene(entries, onBack) } } // Use default strategy return fallbackStrategy.calculateScene(entries, onBack) } } アプリケーションでの使用 さて、すべてをまとめてみましょう. ここでは、底表を使用して実際のアプリケーションでどう見えるかです。 @Composable fun ShoppingApp() { val backStack = rememberNavBackStack(Screen.Home) val router = rememberRouter<Screen>() Nav3Host( backStack = backStack, router = router ) { backStack, onBack, router -> NavDisplay( backStack = backStack, onBack = onBack, // Use our combined strategy sceneStrategy = DelegatedScreenStrategy( strategyMap = mapOf( "bottomsheet" to BottomSheetSceneStrategy(), "dialog" to DialogSceneStrategy() // Navigation 3 already has this strategy ), fallbackStrategy = SinglePaneSceneStrategy() // Regular screens ), entryProvider = entryProvider { entry<Screen.Home> { HomeScreen( onOpenFilters = { // Open filters as bottom sheet router.push(Screen.Filters) } ) } entry<Screen.Product> { screen -> ProductScreen(productId = screen.id) } // Specify that Filters should be bottom sheet entry<Screen.Filters>( metadata = BottomSheetSceneStrategy.bottomSheet() ) { FiltersContent( onApply = { filters -> // Apply filters and close bottom sheet applyFilters(filters) router.pop() } ) } } ) } } 何が起きているのか? 呼ぶとき しかし、メタデータと私たちの戦略のおかげで、UIは、この画面が以前の画面の上部に底表として表示される必要があることを理解し、それを完全に置き換えるのではなく、 router.push(Screen.Filters) 呼ぶとき ルーターの視点から、これは通常のバックナビゲーションですが、視覚的にそれはモダルウィンドウを閉じるように見えます。 router.pop() このアプローチの利点 SceneStrategy を使用すると、いくつかの重要な利点があります。まず、あなたのナビゲーションの論理はシンプルで、あなたはまだ使用しています。 そして スクリーンが正確にどのように表示されるかを考えることなく 第二に、ナビゲーションの状態は一貫している - 底表は、スクリーン回転またはプロセス殺しの間に適切に保存されるバックのもう一つのスクリーンです。 push pop このアプローチは、文脈に応じて同じ画面が異なるように表示される場合に特に役立ちます. For example, a login screen can be a regular screen on first app launch and a modal dialog when attempting to perform an action that requires authorization. たとえば、ログイン画面は、最初のアプリの起動時に通常の画面であり、権限を必要とするアクションを実行しようとしているときにモダルな対話になります。 なぜNAV3ルーターを使うべきか Nav3 ルーターは、Navigation 3 を置き換えたり、新しい機能を追加したりしようとはしません。Nav3 ルーターの仕事は、ナビゲーションの作業を便利かつ予測できるようにすることです。アプリケーションのあらゆる層から使用できるシンプルな API、タイミングの問題の自動処理、および簡単にナビゲーションの論理をテストする能力があります。 同時に、キャップの下で、通常のナビゲーション3はまだすべての機能で動作します:状態の保存、アニメーションのサポート、およびシステム「バック」ボタンの適切な操作。 既にNavigation 3を使用している場合、またはそれに移行する計画がある場合、Nav3 Routerはプロジェクトに不必要な複雑さを加えることなく、この体験をより楽しいものにします。 左利き GitHub リポジトリ: github.com/arttttt/Nav3Router 使用例:Sample folder in the repository を参照