はじめに
Jetpack Navigation 3(以下Nav3)は、Composeによる状態管理を前提に構築された新しい画面遷移ライブラリです。高い柔軟性とカスタマイズ性を備えています。
この記事では、Nav3による画面遷移の設計について、個人的な結論1に至るまでの試行錯誤と、要件に応じた構成の選択基準を整理します。
要件に応じた設計の変化
Nav3はComposeの状態を前提とした疎結合なAPI群であり、android/nav3-recipesでも要件に応じた複数の実装アプローチが示されています。
特に、ナビゲーションバーなどによる画面切り替えの有無、ディープリンクの要件、Scene の扱いといった要素によって、適切な設計は大きく変わります。
試行錯誤
単純なバックスタック
rememberNavBackStack で生成したバックスタックをそのまま NavDisplay に渡す、最もシンプルな構成です。これは、android/nav3-recipes - Basic DSL Recipeで紹介されています。
- 向いている要件: 直線的な画面遷移のみで構成されている
- メリット: 処理をNav3に一任できるため、実装が極めてシンプル
- デメリット: ナビゲーションバーなどの画面切り替えへの対応が困難2
val backStack = rememberNavBackStack(ANavKey)
NavDisplay(
backStack = backStack,
onBack = { backStack.removeLastOrNull() },
entryProvider = entryProvider {
entry<ANavKey> { /*...*/ }
entry<BNavKey> { /*...*/ }
},
)
複数バックスタック
ナビゲーションバーによる画面切り替えに対応する、複数のバックスタックを保持する構成です。トップレベルの選択状態に応じて rememberDecoratedNavEntries で生成したエントリーを切り替えます。これは、android/nav3-recipes - Multiple back stacks recipeやMigrate from Navigation 2 to Navigation 3で紹介されています。
- 向いている要件: トップレベルのバックスタック切り替えのみによる画面遷移で構成されている
- メリット: 状態保持を明示的に管理でき、タブ切り替えなどにも自然に対応可能
- デメリット: 「深い階層(利用規約など)からトップレベル(ホームなど)への遷移」などを実現するには独自の拡張が必要
val topLevel by remember { mutableStateOf(TopLevel1NavKey) }
val backStacks = setOf(TopLevel1NavKey, TopLevel2NavKey).associateWith { key ->
rememberNavBackStack(key)
}
val entryProvider = entryProvider { /*...*/ }
val entries = backStacks.mapValues { (_, stack) ->
rememberDecoratedNavEntries(
backStack = stack,
entryProvider = entryProvider,
)
}
NavDisplay(
entries = entries[topLevel] ?: emptyList(),
onBack = { /*...*/ },
)
木構造による状態定義
より複雑な要件に応えるため、遷移状態を木構造(NavNode)で定義し、現在の活性ノードを保持するアプローチです3。詳細な実装例はDaiji256/android-showcase - navigationに記載しています。
- 向いている要件: ネストした遷移や、表示状態と遷移状態を分離して扱いたいケース
- メリット: 画面の表示・非表示に関わらず遷移状態を一元管理でき、複雑な制御に強い
-
デメリット: 基盤の設計コストが高く、特異な要件に対して
NavNode自体の拡張を強いられる
class NavNode(val key: NavKey) {
var currentChild: NavNode? by mutableStateOf<NavNode?>(null)
val children: SnapshotStateSet<NavNode> = mutableStateSetOf()
}
val node: NavNode = // ...
val inactiveBackStack: List<NavKey> = // ...
val activeBackStack: List<NavKey> = // ...
val allEntries = rememberDecoratedNavEntries(
backStack = inactiveBackStack + activeBackStack,
entryProvider = entryProvider,
)
val entries = remember(allEntries, inactiveBackStack, activeBackStack) {
allEntries.takeLast(activeBackStack.size)
}
ドメイン特化の専用設計(現在の結論)
NavNode のような汎用化は自由度が高い半面、ディープリンクや複数画面の同時pop、未知の要件4に対しては状態管理が肥大化しがちです。これは実質的に「Nav3の上にNav2のような汎用APIを再構築している5」と言えます。
Nav3の真価は、巨大なAPIではなく「疎結合な最小限のAPI群」である点にあります。そのため、「汎用的な画面遷移APIを作るのではなく、アプリのドメイン(要件)に特化した専用の設計をその都度定義する」ことが、Nav3の最も素直な活用法であるという結論に至りました。
APIの使い分けや設計のポイントについては、Nav3は状態の保持と描画を分けて考えると設計しやすいで紹介しています。
- 向いている要件: アプリ固有の遷移要件が強く、専用設計の方がドメインを素直に表現できるケース
- メリット: 余計な抽象化コストを抑え、複雑な固有要件にシンプルに対応できる
- デメリット: 汎用性がなく、遷移ロジックとドメインが密結合になる
まとめ
Nav3による画面遷移は、実装者が設計を完全にコントロールできます。無理にNav2のような汎用APIを構築するのではなく、公式レシピが示すようにアプリの要件に合わせて状態設計を行うことで、Nav3の強みを最大限に活かすことができます。
参考文献
- android/nav3-recipes | GitHub
- Android Developers Blog: Jetpack Navigation 3 is stable | Android Developers Blog
- Migrate from Navigation 2 to Navigation 3 | App architecture | Android Developers
- Nav3は状態の保持と描画を分けて考えると設計しやすい | Qiita
- Daiji256/android-showcase - navigation | GitHub