navigation3 について
Jetpack ComposeでNavigation 3を使い、状態駆動のナビゲーションを実装するスキルです。バックスタック、ViewModelのスコープ、アダプティブレイアウト、ディープリンク、状態復元、アニメーション、テスト、Navigation Composeからの移行を扱います。
主な機能
- 01シリアライズ可能なナビゲーションキー
- 02可変状態のバックスタック
- 03ViewModel用エントリーデコレーター
- 04アダプティブレイアウトとディープリンク
- 05状態復元とテスト
活用シーン
- 01遷移先をアプリケーション状態としてモデル化
- 02ナビゲーションエントリーごとにViewModelをスコープ
- 03スマートフォン、タブレット、折りたたみ端末のレイアウトに対応
- 04バックスタックの変更と画面描画をテスト
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ new-silvermoon/ awesome-android-agent-skills --skill navigation3インストール後、Claude Code で/navigation3を入力して読み込まれたか確認します。
詳細
Navigation 3
概要
Navigation 3 を使用して、Jetpack Compose で状態駆動型のナビゲーションを実装します。Navigation Compose とは異なり、Navigation 3 は NavController を介さず、アプリケーションの状態としてナビゲーションをモデル化します。このスキルでは、ナビゲーションキー、バックスタック管理、ViewModel のスコープ設定、エントリデコレーター、アダプティブレイアウト、ディープリンク、アニメーション、状態復元、テストについて説明します。
セットアップ
Navigation 3 の依存関係を追加します。
// build.gradle.kts
dependencies {
implementation("androidx.navigation3:navigation3-runtime:1.0.0-alpha08")
implementation("androidx.navigation3:navigation3-ui:1.0.0-alpha08")
// Lifecycle integration
implementation("androidx.lifecycle:lifecycle-viewmodel-navigation3:2.9.2")
// Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.8.1")
}
// Enable serialization plugin
plugins {
kotlin("plugin.serialization") version "2.2.0"
}
コアコンセプト
1. ナビゲーションキーを定義する
ナビゲーション先は、不変でシリアライズ可能なオブジェクトで表します。
import kotlinx.serialization.Serializable
@Serializable
data object Home
@Serializable
data class Profile(
val userId: String
)
@Serializable
data class Product(
val productId: String,
val showReviews: Boolean = false
)
@Serializable
data object Settings
キーがナビゲーションの状態になります。
2. バックスタックを作成する
Navigation 3 では、NavController の代わりに変更可能な状態バックスタックを使用します。
@Composable
fun MyApp() {
val backStack = remember {
mutableStateListOf<Any>(Home)
}
AppNavDisplay(backStack)
}
3. NavDisplay を作成する
@Composable
fun AppNavDisplay(
backStack: SnapshotStateList<Any>
) {
NavDisplay(
backStack = backStack,
onBack = {
if (backStack.size > 1) {
backStack.removeLast()
}
}
) { key ->
when (key) {
Home ->
HomeScreen(
onProfileClick = {
backStack += Profile(it)
}
)
is Profile ->
ProfileScreen(
userId = key.userId
)
is Product ->
ProductScreen(
productId = key.productId,
showReviews = key.showReviews
)
Settings ->
SettingsScreen()
}
}
}
ナビゲーションパターン
次の画面へ移動する
backStack += Profile("user123")
戻る
backStack.removeLast()
現在の画面を置き換える
backStack[backStack.lastIndex] = Home
バックスタックをクリアする
backStack.clear()
backStack += Home
ルート画面まで戻る
while (backStack.size > 1) {
backStack.removeLast()
}
引数の処理
引数を取得する
引数はすでにナビゲーションキーに含まれています。
when (val key = currentKey) {
is Profile -> {
ProfileScreen(
userId = key.userId
)
}
is Product -> {
ProductScreen(
productId = key.productId
)
}
}
オブジェクトではなく ID を渡す
// CORRECT
backStack += Profile(user.id)
// Fetch object inside ViewModel
class ProfileViewModel(
savedStateHandle: SavedStateHandle,
repository: UserRepository
) : ViewModel() {
val profile = savedStateHandle.toRoute<Profile>()
val user =
repository.getUser(profile.userId)
}
// INCORRECT
backStack += User(...)
backStack += ProductRepository(...)
backStack += ProductViewModel(...)
ViewModel の統合
Navigation 3 では、エントリデコレーターを使用して ViewModel のスコープを設定します。
NavDisplay(
backStack = backStack,
entryDecorators = listOf(
rememberSceneSetupNavEntryDecorator(),
rememberSavedStateNavEntryDecorator(),
rememberViewModelStoreNavEntryDecorator()
)
) { key ->
// destinations
}
ViewModel の例
class ProfileViewModel(
savedStateHandle: SavedStateHandle
) : ViewModel() {
val profile =
savedStateHandle.toRoute<Profile>()
}
エントリデコレーター
Navigation 3 では、ライフサイクル機能を付与するためにデコレーターを使用します。
シーンの設定
rememberSceneSetupNavEntryDecorator()
各エントリのナビゲーションシーンを作成します。
保存状態
rememberSavedStateNavEntryDecorator()
プロセス再作成後に、遷移先の状態を自動的に復元します。
ViewModel ストア
rememberViewModelStoreNavEntryDecorator()
各ナビゲーションエントリに ViewModel のスコープを設定します。
アダプティブナビゲーション
Navigation 3 は Material Adaptive レイアウトと統合されています。
NavDisplay(
backStack = backStack,
sceneStrategy = rememberListDetailSceneStrategy()
)
アダプティブシーン戦略を使用すると、次のレイアウトに自動的に切り替えられます。
- シングルペイン(スマートフォン)
- 2 ペイン(タブレット)
- 折りたたみ式デバイス
ディープリンク
ディープリンクはナビゲーションキーに解決されるようにします。
fun handleDeepLink(uri: Uri) {
val userId =
uri.lastPathSegment ?: return
backStack += Profile(userId)
}
ルート文字列を手動で構築することは避けてください。
アニメーション
ナビゲーションの遷移は、シーン遷移を使用して定義します。
NavDisplay(
backStack = backStack,
transitionSpec = {
fadeIn() togetherWith fadeOut()
}
)
Navigation 3 のアニメーション API は、アルファ版の間に変更される可能性があります。
状態の復元
ナビゲーションキーはシリアライズ可能で、自動的に復元されます。
val backStack = rememberSaveable(
saver = navBackStackSaver()
) {
mutableStateListOf(Home)
}
キーがシリアライズ可能であることを必ず確認してください。
テスト
状態駆動型であるため、ナビゲーションのテストは簡単です。
例
@Test
fun navigateToProfile() {
val backStack =
mutableStateListOf<Any>(Home)
backStack += Profile("123")
assertEquals(
Profile("123"),
backStack.last()
)
}
Compose UI テストでは、現在のバックスタックを調べて画面のレンダリングを検証できます。
Navigation Compose からの移行
| Navigation Compose | Navigation 3 |
|---|---|
NavController | 変更可能なバックスタック |
NavHost | NavDisplay |
navigate() | backStack += Key |
popBackStack() | removeLast() |
| 文字列ルート | シリアライズ可能なキー |
composable() | when(key) |
| ナビゲーショングラフ | 状態駆動型の遷移先 |
推奨されるプロジェクト構成
navigation/
AppNavigation.kt
NavigationKeys.kt
NavigationDisplay.kt
feature/
home/
profile/
settings/
重要なルール
実施すること
- 不変でシリアライズ可能なキーを使用する
- ナビゲーションの状態を Compose 内に保持する
- 複雑なオブジェクトではなく ID を渡す
- エントリデコレーターを使用して ViewModel のスコープを設定する
- 状態の復元には
rememberSaveableを使用する - ナビゲーションを監視可能なアプリケーション状態としてモデル化する
実施しないこと
- 文字列ルートを使用する
- ナビゲーション経由でリポジトリや ViewModel を渡す
- ナビゲーションキーを変更する
- バックスタックにビジネスオブジェクトを保存する
- 再コンポジション時にバックスタックを再作成する
- Navigation Compose API と Navigation 3 API を混在させる
参考資料
- Android Navigation 3 のドキュメント
- Navigation 3 のサンプル
- Lifecycle ViewModel Navigation 3 のドキュメント
- Material 3 Adaptive Navigation のドキュメント