Skip to content

Commit dd543d1

Browse files
committed
Migrate from Navigation 2 to Navigation 3
This commit migrates the app from Navigation 2 to Navigation 3, following the newly added `migration-guide.md`. Key changes include: * Replacing `NavHost` with `NavDisplay`. * Introducing `NavigationState` to manage navigation back stacks and `Navigator` to handle navigation events, replacing `NavController`. * Defining routes as type-safe `NavKey` objects. * Moving destination definitions from `NavHost`'s graph builder to an `entryProvider`. * Adding Navigation 3 dependencies and removing the old `navigation-compose` library. * Enabling the Kotlin serialization plugin.
1 parent cdb6de4 commit dd543d1

File tree

7 files changed

+770
-12
lines changed

7 files changed

+770
-12
lines changed

app/build.gradle.kts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
* http://www.apache.org/licenses/LICENSE-2.0
99
*
1010
* Unless required by applicable law or agreed to in writing, software
11-
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* distributed under the License is distributed on an "ASIS" BASIS,
1212
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
@@ -22,6 +22,7 @@ plugins {
2222
alias(libs.plugins.hilt.gradle)
2323
alias(libs.plugins.ksp)
2424
alias(libs.plugins.compose.compiler)
25+
alias(libs.plugins.kotlin.serialization)
2526
}
2627

2728
android {
@@ -101,7 +102,6 @@ dependencies {
101102
// Arch Components
102103
implementation(libs.androidx.lifecycle.runtime.compose)
103104
implementation(libs.androidx.lifecycle.viewmodel.compose)
104-
implementation(libs.androidx.navigation.compose)
105105
implementation(libs.androidx.hilt.navigation.compose)
106106
implementation(libs.androidx.room.runtime)
107107
implementation(libs.androidx.room.ktx)
@@ -126,4 +126,9 @@ dependencies {
126126
androidTestImplementation(libs.androidx.test.core)
127127
androidTestImplementation(libs.androidx.test.ext.junit)
128128
androidTestImplementation(libs.androidx.test.runner)
129+
130+
// Navigation 3
131+
implementation(libs.androidx.navigation3.ui)
132+
implementation(libs.androidx.navigation3.runtime)
133+
implementation(libs.androidx.lifecycle.viewmodel.navigation3)
129134
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
* Copyright (C) 2024 The Android Open Source Project
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package android.template.ui
18+
19+
import androidx.navigation3.runtime.NavKey
20+
import kotlinx.serialization.Serializable
21+
22+
@Serializable
23+
data object Main : NavKey

app/src/main/java/android/template/ui/Navigation.kt

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,21 +16,32 @@
1616

1717
package android.template.ui
1818

19+
import android.template.ui.mymodel.MyModelScreen
1920
import androidx.compose.foundation.layout.padding
2021
import androidx.compose.runtime.Composable
22+
import androidx.compose.runtime.remember
2123
import androidx.compose.ui.Modifier
2224
import androidx.compose.ui.unit.dp
23-
import androidx.navigation.compose.NavHost
24-
import androidx.navigation.compose.composable
25-
import androidx.navigation.compose.rememberNavController
26-
import android.template.ui.mymodel.MyModelScreen
25+
import androidx.navigation3.runtime.NavKey
26+
import androidx.navigation3.runtime.entryProvider
27+
import androidx.navigation3.ui.NavDisplay
2728

2829
@Composable
2930
fun MainNavigation() {
30-
val navController = rememberNavController()
31+
val navigationState = rememberNavigationState(
32+
startRoute = Main,
33+
topLevelRoutes = setOf(Main)
34+
)
35+
val navigator = remember { Navigator(navigationState) }
3136

32-
NavHost(navController = navController, startDestination = "main") {
33-
composable("main") { MyModelScreen(modifier = Modifier.padding(16.dp)) }
34-
// TODO: Add more destinations
37+
val entryProvider = entryProvider<NavKey> {
38+
entry<Main> {
39+
MyModelScreen(modifier = Modifier.padding(16.dp))
40+
}
3541
}
42+
43+
NavDisplay(
44+
entries = navigationState.toEntries(entryProvider),
45+
onBack = { navigator.goBack() }
46+
)
3647
}
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
2+
package android.template.ui
3+
4+
import androidx.compose.runtime.Composable
5+
import androidx.compose.runtime.MutableState
6+
import androidx.compose.runtime.getValue
7+
import androidx.compose.runtime.mutableStateOf
8+
import androidx.compose.runtime.remember
9+
import androidx.compose.runtime.saveable.rememberSerializable
10+
import androidx.compose.runtime.setValue
11+
import androidx.compose.runtime.snapshots.SnapshotStateList
12+
import androidx.compose.runtime.toMutableStateList
13+
import androidx.navigation3.runtime.NavBackStack
14+
import androidx.navigation3.runtime.NavEntry
15+
import androidx.navigation3.runtime.NavKey
16+
import androidx.navigation3.runtime.rememberDecoratedNavEntries
17+
import androidx.navigation3.runtime.rememberNavBackStack
18+
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
19+
import androidx.navigation3.runtime.serialization.NavKeySerializer
20+
import androidx.savedstate.compose.serialization.serializers.MutableStateSerializer
21+
22+
/**
23+
* Create a navigation state that persists config changes and process death.
24+
*/
25+
@Composable
26+
fun rememberNavigationState(
27+
startRoute: NavKey,
28+
topLevelRoutes: Set<NavKey>
29+
): NavigationState {
30+
31+
val topLevelRoute = rememberSerializable(
32+
startRoute, topLevelRoutes,
33+
serializer = MutableStateSerializer(NavKeySerializer())
34+
) {
35+
mutableStateOf(startRoute)
36+
}
37+
38+
val backStacks = topLevelRoutes.associateWith { key -> rememberNavBackStack(key) }
39+
40+
return remember(startRoute, topLevelRoutes) {
41+
NavigationState(
42+
startRoute = startRoute,
43+
topLevelRoute = topLevelRoute,
44+
backStacks = backStacks
45+
)
46+
}
47+
}
48+
49+
/**
50+
* State holder for navigation state.
51+
*
52+
* @param startRoute - the start route. The user will exit the app through this route.
53+
* @param topLevelRoute - the current top level route
54+
* @param backStacks - the back stacks for each top level route
55+
*/
56+
class NavigationState(
57+
val startRoute: NavKey,
58+
topLevelRoute: MutableState<NavKey>,
59+
val backStacks: Map<NavKey, NavBackStack<NavKey>>
60+
) {
61+
var topLevelRoute: NavKey by topLevelRoute
62+
val stacksInUse: List<NavKey>
63+
get() = if (topLevelRoute == startRoute) {
64+
listOf(startRoute)
65+
} else {
66+
listOf(startRoute, topLevelRoute)
67+
}
68+
}
69+
70+
/**
71+
* Convert NavigationState into NavEntries.
72+
*/
73+
@Composable
74+
fun NavigationState.toEntries(
75+
entryProvider: (NavKey) -> NavEntry<NavKey>
76+
): SnapshotStateList<NavEntry<NavKey>> {
77+
78+
val decoratedEntries = backStacks.mapValues { (_, stack) ->
79+
val decorators = listOf(
80+
rememberSaveableStateHolderNavEntryDecorator<NavKey>(),
81+
)
82+
rememberDecoratedNavEntries(
83+
backStack = stack,
84+
entryDecorators = decorators,
85+
entryProvider = entryProvider
86+
)
87+
}
88+
89+
return stacksInUse
90+
.flatMap { decoratedEntries[it] ?: emptyList() }
91+
.toMutableStateList()
92+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
2+
package android.template.ui
3+
4+
import androidx.navigation3.runtime.NavKey
5+
6+
/**
7+
* Handles navigation events (forward and back) by updating the navigation state.
8+
*/
9+
class Navigator(val state: NavigationState){
10+
fun navigate(route: NavKey){
11+
if (route in state.backStacks.keys){
12+
// This is a top level route, just switch to it.
13+
state.topLevelRoute = route
14+
} else {
15+
state.backStacks[state.topLevelRoute]?.add(route)
16+
}
17+
}
18+
19+
fun goBack(){
20+
val currentStack = state.backStacks[state.topLevelRoute] ?:
21+
error("Stack for ${state.topLevelRoute} not found")
22+
val currentRoute = currentStack.last()
23+
24+
// If we're at the base of the current route, go back to the start route stack.
25+
if (currentRoute == state.topLevelRoute){
26+
state.topLevelRoute = state.startRoute
27+
} else {
28+
currentStack.removeLastOrNull()
29+
}
30+
}
31+
}

gradle/libs.versions.toml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ androidxLifecycle = "2.10.0"
55
androidxActivity = "1.12.0"
66
androidxComposeBom = "2025.11.01"
77
androidxHilt = "1.3.0"
8-
androidxNavigation = "2.9.6"
98
androidxRoom = "2.8.4"
109
androidxTest = "1.7.0"
1110
androidxTestExt = "1.3.0"
@@ -15,6 +14,8 @@ hilt = "2.57.2"
1514
junit = "4.13.2"
1615
kotlin = "2.2.21"
1716
ksp = "2.3.2"
17+
nav3Core = "1.0.0"
18+
lifecycleViewmodelNav3 = "2.10.0"
1819

1920
[libraries]
2021
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidxCore" }
@@ -30,7 +31,6 @@ androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-com
3031
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidxLifecycle" }
3132
androidx-lifecycle-runtime-compose = { module = "androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidxLifecycle" }
3233
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "androidxLifecycle" }
33-
androidx-navigation-compose = { module = "androidx.navigation:navigation-compose", version.ref = "androidxNavigation" }
3434
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidxRoom" }
3535
androidx-room-ktx = { module = "androidx.room:room-ktx", version.ref = "androidxRoom" }
3636
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "androidxRoom" }
@@ -44,6 +44,9 @@ hilt-compiler = { module = "com.google.dagger:hilt-compiler", version.ref = "hil
4444
hilt-gradle-plugin = { module = "com.google.dagger:hilt-android-gradle-plugin", version.ref = "hilt" }
4545
junit = { module = "junit:junit", version.ref = "junit" }
4646
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
47+
androidx-navigation3-runtime = { module = "androidx.navigation3:navigation3-runtime", version.ref = "nav3Core" }
48+
androidx-navigation3-ui = { module = "androidx.navigation3:navigation3-ui", version.ref = "nav3Core" }
49+
androidx-lifecycle-viewmodel-navigation3 = { module = "androidx.lifecycle:lifecycle-viewmodel-navigation3", version.ref = "lifecycleViewmodelNav3" }
4750

4851
[plugins]
4952
android-application = { id = "com.android.application", version.ref = "androidGradlePlugin" }
@@ -53,3 +56,4 @@ kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
5356
kotlin-kapt = { id = "org.jetbrains.kotlin.kapt", version.ref = "kotlin" }
5457
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp"}
5558
hilt-gradle = { id = "com.google.dagger.hilt.android", version.ref = "hilt" }
59+
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }

0 commit comments

Comments
 (0)