Skip to content

Commit 5d653e5

Browse files
committed
[#617] Migrate sample-compose to navigation 3
1 parent 80db41c commit 5d653e5

20 files changed

Lines changed: 167 additions & 183 deletions

File tree

sample-compose/app/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ plugins {
33
alias(libs.plugins.compose.compiler)
44
alias(libs.plugins.kotlin.android)
55
alias(libs.plugins.kotlin.parcelize)
6+
alias(libs.plugins.kotlin.serialization)
67
alias(libs.plugins.ksp)
78
alias(libs.plugins.hilt)
89
alias(libs.plugins.kover)
@@ -133,6 +134,7 @@ dependencies {
133134

134135
implementation(libs.kotlin.stdlib)
135136
implementation(libs.kotlinx.coroutines.core)
137+
implementation(libs.kotlinx.serialization.json)
136138

137139
debugImplementation(libs.chucker)
138140
releaseImplementation(libs.chucker.noOp)

sample-compose/app/src/androidTest/java/co/nimblehq/sample/compose/ui/screens/main/home/HomeScreenTest.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import co.nimblehq.sample.compose.domain.usecase.IsFirstTimeLaunchPreferencesUse
1313
import co.nimblehq.sample.compose.domain.usecase.UpdateFirstTimeLaunchPreferencesUseCase
1414
import co.nimblehq.sample.compose.test.MockUtil
1515
import co.nimblehq.sample.compose.test.TestDispatchersProvider
16-
import co.nimblehq.sample.compose.ui.base.BaseDestination
16+
import androidx.navigation3.runtime.NavKey
1717
import co.nimblehq.sample.compose.ui.screens.MainActivity
1818
import co.nimblehq.sample.compose.ui.screens.main.MainDestination
1919
import co.nimblehq.sample.compose.ui.theme.ComposeTheme
@@ -43,7 +43,7 @@ class HomeScreenTest {
4343
private val mockUpdateFirstTimeLaunchPreferencesUseCase: UpdateFirstTimeLaunchPreferencesUseCase = mockk()
4444

4545
private lateinit var viewModel: HomeViewModel
46-
private var expectedDestination: BaseDestination? = null
46+
private var expectedDestination: NavKey? = null
4747

4848
@Before
4949
fun setUp() {
@@ -74,7 +74,7 @@ class HomeScreenTest {
7474
fun when_clicking_on_a_list_item__it_navigates_to_Second_screen() = initComposable {
7575
onNodeWithText("1").performClick()
7676

77-
assertEquals(expectedDestination, MainDestination.Second)
77+
assertEquals((expectedDestination as? MainDestination.Second)?.id, "1")
7878
}
7979

8080
private fun initComposable(
Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
package co.nimblehq.sample.compose.ui
22

3-
import co.nimblehq.sample.compose.ui.base.BaseDestination
3+
import androidx.navigation3.runtime.NavKey
4+
import kotlinx.serialization.Serializable
45

5-
sealed class AppDestination {
6+
sealed interface AppDestination : NavKey {
67

7-
object RootNavGraph : BaseDestination("rootNavGraph")
8-
9-
object MainNavGraph : BaseDestination("mainNavGraph")
8+
@Serializable
9+
data object MainNavGraph : AppDestination
1010
}
Lines changed: 35 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,45 @@
11
package co.nimblehq.sample.compose.ui
22

33
import androidx.compose.runtime.Composable
4-
import androidx.navigation.NavBackStackEntry
5-
import androidx.navigation.NavGraphBuilder
6-
import androidx.navigation.NavHostController
7-
import androidx.navigation.compose.NavHost
8-
import androidx.navigation.compose.composable
9-
import androidx.navigation.navDeepLink
10-
import co.nimblehq.sample.compose.ui.base.BaseDestination
11-
import co.nimblehq.sample.compose.ui.screens.main.mainNavGraph
4+
import androidx.navigation3.runtime.NavEntry
5+
import androidx.navigation3.runtime.rememberSaveableStateHolderNavEntryDecorator
6+
import androidx.navigation3.ui.NavDisplay
7+
import co.nimblehq.sample.compose.ui.navigation.Navigator
8+
import co.nimblehq.sample.compose.ui.navigation.NavigationState
9+
import co.nimblehq.sample.compose.ui.screens.main.MainDestination
10+
import co.nimblehq.sample.compose.ui.screens.main.home.HomeScreen
11+
import co.nimblehq.sample.compose.ui.screens.main.second.SecondScreen
12+
import co.nimblehq.sample.compose.ui.screens.main.third.ThirdScreen
1213

1314
@Composable
1415
fun AppNavGraph(
15-
navController: NavHostController,
16+
navigationState: NavigationState<MainDestination>,
17+
navigator: Navigator<MainDestination>,
1618
) {
17-
NavHost(
18-
navController = navController,
19-
route = AppDestination.RootNavGraph.route,
20-
startDestination = AppDestination.MainNavGraph.destination
21-
) {
22-
mainNavGraph(navController = navController)
23-
}
24-
}
25-
26-
fun NavGraphBuilder.composable(
27-
destination: BaseDestination,
28-
content: @Composable (NavBackStackEntry) -> Unit,
29-
) {
30-
composable(
31-
route = destination.route,
32-
arguments = destination.arguments,
33-
deepLinks = destination.deepLinks.map {
34-
navDeepLink {
35-
uriPattern = it
36-
}
37-
},
38-
content = content
39-
)
40-
}
41-
42-
/**
43-
* Navigate to provided [BaseDestination] with a Pair of key value String and Data [parcel]
44-
* Caution to use this method. This method use savedStateHandle to store the Parcelable data.
45-
* When previousBackstackEntry is popped out from navigation stack, savedStateHandle will return null and cannot retrieve data.
46-
* eg.Login -> Home, the Login screen will be popped from the back-stack on logging in successfully.
47-
*/
48-
fun NavHostController.navigate(destination: BaseDestination, parcel: Pair<String, Any?>? = null) {
49-
when (destination) {
50-
is BaseDestination.Up -> {
51-
destination.results.forEach { (key, value) ->
52-
previousBackStackEntry?.savedStateHandle?.set(key, value)
19+
NavDisplay(
20+
backStack = navigationState.backStack,
21+
onBack = { navigator.goBack() },
22+
entryDecorators = listOf(rememberSaveableStateHolderNavEntryDecorator()),
23+
entryProvider = { key ->
24+
when (key) {
25+
is MainDestination.Home -> NavEntry(key) {
26+
HomeScreen(
27+
navigator = { destination -> navigator.navigate(destination as MainDestination) }
28+
)
29+
}
30+
is MainDestination.Second -> NavEntry(key) {
31+
SecondScreen(
32+
navigator = { navigator.goBack() },
33+
id = key.id
34+
)
35+
}
36+
is MainDestination.Third -> NavEntry(key) {
37+
ThirdScreen(
38+
navigator = {},
39+
model = key.model
40+
)
41+
}
5342
}
54-
navigateUp()
5543
}
56-
else -> {
57-
parcel?.let { (key, value) ->
58-
currentBackStackEntry?.savedStateHandle?.set(key, value)
59-
}
60-
navigate(route = destination.destination)
61-
}
62-
}
44+
)
6345
}

sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseDestination.kt

Lines changed: 0 additions & 26 deletions
This file was deleted.

sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/base/BaseViewModel.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package co.nimblehq.sample.compose.ui.base
22

33
import androidx.lifecycle.ViewModel
44
import androidx.lifecycle.viewModelScope
5+
import androidx.navigation3.runtime.NavKey
56
import kotlinx.coroutines.*
67
import kotlinx.coroutines.flow.*
78
import kotlin.coroutines.CoroutineContext
@@ -18,7 +19,7 @@ abstract class BaseViewModel : ViewModel() {
1819
protected val _error = MutableSharedFlow<Throwable>()
1920
val error = _error.asSharedFlow()
2021

21-
protected val _navigator = MutableSharedFlow<BaseDestination>()
22+
protected val _navigator = MutableSharedFlow<NavKey>()
2223
val navigator = _navigator.asSharedFlow()
2324

2425
/**

sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/models/UiModel.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ package co.nimblehq.sample.compose.ui.models
33
import android.os.Parcelable
44
import co.nimblehq.sample.compose.domain.models.Model
55
import kotlinx.parcelize.Parcelize
6+
import kotlinx.serialization.Serializable
67

8+
@Serializable
79
@Parcelize
810
data class UiModel(
911
val id: String,
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
package co.nimblehq.sample.compose.ui.navigation
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.mutableStateListOf
5+
import androidx.compose.runtime.remember
6+
import androidx.navigation3.runtime.NavKey
7+
8+
/**
9+
* Remember a [NavigationState].
10+
* Note: Navigation3's NavDisplay handles state restoration internally,
11+
* so we don't need to use rememberSaveable here.
12+
*/
13+
@Composable
14+
fun <T : NavKey> rememberNavigationState(
15+
startDestination: T,
16+
): NavigationState<T> = remember {
17+
NavigationState(startDestination)
18+
}
19+
20+
/**
21+
* Holds the navigation state including the back stack.
22+
*/
23+
class NavigationState<T : NavKey>(
24+
startDestination: T,
25+
) {
26+
val backStack = mutableStateListOf(startDestination)
27+
28+
val currentDestination: T
29+
get() = backStack.last()
30+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package co.nimblehq.sample.compose.ui.navigation
2+
3+
import androidx.navigation3.runtime.NavKey
4+
5+
/**
6+
* Navigator class to handle navigation actions.
7+
*/
8+
class Navigator<T : NavKey>(
9+
private val navigationState: NavigationState<T>,
10+
) {
11+
/**
12+
* Navigate to a destination.
13+
*/
14+
fun navigate(destination: T) {
15+
navigationState.backStack.add(destination)
16+
}
17+
18+
/**
19+
* Navigate back. Returns true if navigation was successful.
20+
*/
21+
fun goBack(): Boolean {
22+
return if (navigationState.backStack.size > 1) {
23+
navigationState.backStack.removeLast()
24+
true
25+
} else {
26+
false
27+
}
28+
}
29+
30+
/**
31+
* Pop back stack up to the specified destination (inclusive or exclusive).
32+
*/
33+
fun popBackTo(destination: T, inclusive: Boolean = false) {
34+
val index = navigationState.backStack.lastIndexOf(destination)
35+
if (index != -1) {
36+
val removeCount = navigationState.backStack.size - index - if (inclusive) 0 else 1
37+
repeat(removeCount) {
38+
if (navigationState.backStack.size > 1) {
39+
navigationState.backStack.removeLast()
40+
}
41+
}
42+
}
43+
}
44+
}

sample-compose/app/src/main/java/co/nimblehq/sample/compose/ui/screens/MainActivity.kt

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,10 @@ package co.nimblehq.sample.compose.ui.screens
33
import android.os.Bundle
44
import androidx.activity.ComponentActivity
55
import androidx.activity.compose.setContent
6-
import androidx.navigation.compose.rememberNavController
76
import co.nimblehq.sample.compose.ui.AppNavGraph
7+
import co.nimblehq.sample.compose.ui.navigation.Navigator
8+
import co.nimblehq.sample.compose.ui.navigation.rememberNavigationState
9+
import co.nimblehq.sample.compose.ui.screens.main.MainDestination
810
import co.nimblehq.sample.compose.ui.theme.ComposeTheme
911
import dagger.hilt.android.AndroidEntryPoint
1012

@@ -15,7 +17,12 @@ class MainActivity : ComponentActivity() {
1517
super.onCreate(savedInstanceState)
1618
setContent {
1719
ComposeTheme {
18-
AppNavGraph(navController = rememberNavController())
20+
val navigationState = rememberNavigationState<MainDestination>(MainDestination.Home)
21+
val navigator = Navigator(navigationState)
22+
AppNavGraph(
23+
navigationState = navigationState,
24+
navigator = navigator
25+
)
1926
}
2027
}
2128
}

0 commit comments

Comments
 (0)