Skip to content

Commit 22135ee

Browse files
committed
feat: navigating to type safe navigation in compose
1 parent fc57b63 commit 22135ee

File tree

21 files changed

+102
-89
lines changed

21 files changed

+102
-89
lines changed

app/src/main/kotlin/com/espressodev/gptmap/GmApp.kt

+6-6
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ import androidx.navigation.NavHostController
3535
import androidx.navigation.compose.rememberNavController
3636
import com.espressodev.gptmap.core.common.NetworkMonitor
3737
import com.espressodev.gptmap.core.common.snackbar.SnackbarManager
38-
import com.espressodev.gptmap.feature.login.LoginRoute
39-
import com.espressodev.gptmap.feature.map.MapRouteWithArg
38+
import com.espressodev.gptmap.feature.login.Login
39+
import com.espressodev.gptmap.feature.map.Map
4040
import com.espressodev.gptmap.navigation.GmNavHost
4141
import com.espressodev.gptmap.navigation.TopLevelDestination
4242
import kotlinx.coroutines.CoroutineScope
@@ -49,9 +49,9 @@ fun GmApp(
4949
accountState: AccountState,
5050
appState: GmAppState = rememberAppState(networkMonitor = networkMonitor)
5151
) {
52-
val startDestination = when (accountState) {
53-
AccountState.UserAlreadySignIn -> MapRouteWithArg
54-
else -> LoginRoute
52+
val startDestination: Any = when (accountState) {
53+
AccountState.UserAlreadySignIn -> Map()
54+
else -> Login
5555
}
5656

5757
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
@@ -153,7 +153,7 @@ fun RowScope.GmNavigationBarItem(
153153

154154
private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) =
155155
this?.hierarchy?.any {
156-
it.route?.contains(destination.name, ignoreCase = true) == true
156+
it.route?.substringAfterLast(".")?.contains(destination.name, ignoreCase = true) == true
157157
} == true
158158

159159
@Composable

app/src/main/kotlin/com/espressodev/gptmap/GmAppState.kt

+12-11
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,17 @@ import androidx.navigation.navOptions
1212
import com.espressodev.gptmap.core.common.NetworkMonitor
1313
import com.espressodev.gptmap.core.common.snackbar.SnackbarManager
1414
import com.espressodev.gptmap.core.common.snackbar.SnackbarMessage.Companion.toMessage
15-
import com.espressodev.gptmap.feature.favourite.FavouriteRoute
15+
import com.espressodev.gptmap.feature.favourite.Favourite
1616
import com.espressodev.gptmap.feature.favourite.navigateToFavourite
17-
import com.espressodev.gptmap.feature.map.MapRouteWithArg
17+
import com.espressodev.gptmap.feature.map.Map
1818
import com.espressodev.gptmap.feature.map.navigateToMap
19-
import com.espressodev.gptmap.feature.screenshot_gallery.ScreenshotGalleryRoute
19+
import com.espressodev.gptmap.feature.map.toDestinationString
20+
import com.espressodev.gptmap.feature.screenshot_gallery.ScreenshotGallery
2021
import com.espressodev.gptmap.feature.screenshot_gallery.navigateToScreenshotGallery
2122
import com.espressodev.gptmap.navigation.TopLevelDestination
2223
import com.espressodev.gptmap.navigation.TopLevelDestination.FAVOURITE
2324
import com.espressodev.gptmap.navigation.TopLevelDestination.MAP
24-
import com.espressodev.gptmap.navigation.TopLevelDestination.SCREENSHOT_GALLERY
25+
import com.espressodev.gptmap.navigation.TopLevelDestination.SCREENSHOTGALLERY
2526
import kotlinx.coroutines.CoroutineScope
2627
import kotlinx.coroutines.flow.SharingStarted
2728
import kotlinx.coroutines.flow.filterNotNull
@@ -36,7 +37,7 @@ class GmAppState(
3637
val snackbarHostState: SnackbarHostState,
3738
private val snackbarManager: SnackbarManager,
3839
private val resources: Resources,
39-
coroutineScope: CoroutineScope
40+
coroutineScope: CoroutineScope,
4041
) {
4142
init {
4243
coroutineScope.launch {
@@ -67,24 +68,24 @@ class GmAppState(
6768
.currentBackStackEntryAsState().value?.destination
6869

6970
val currentTopLevelDestination: TopLevelDestination?
70-
@Composable get() = when (currentDestination?.route) {
71-
MapRouteWithArg -> MAP
72-
ScreenshotGalleryRoute -> SCREENSHOT_GALLERY
73-
FavouriteRoute -> FAVOURITE
71+
@Composable get() = when (currentDestination?.route?.substringAfterLast(".")) {
72+
Map().toDestinationString() -> MAP
73+
ScreenshotGallery.toString() -> SCREENSHOTGALLERY
74+
Favourite.toString() -> FAVOURITE
7475
else -> null
7576
}
7677

7778
fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) {
7879
val topLevelNavOptions = navOptions {
79-
popUpTo(MapRouteWithArg) {
80+
popUpTo(Map()) {
8081
saveState = true
8182
}
8283
launchSingleTop = true
8384
restoreState = true
8485
}
8586
when (topLevelDestination) {
8687
MAP -> navController.navigateToMap(navOptions = topLevelNavOptions)
87-
SCREENSHOT_GALLERY -> navController.navigateToScreenshotGallery(topLevelNavOptions)
88+
SCREENSHOTGALLERY -> navController.navigateToScreenshotGallery(topLevelNavOptions)
8889
FAVOURITE -> navController.navigateToFavourite(topLevelNavOptions)
8990
}
9091
}

app/src/main/kotlin/com/espressodev/gptmap/navigation/GmNavHost.kt

+3-3
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import com.espressodev.gptmap.feature.forgot_password.forgotPasswordScreen
1515
import com.espressodev.gptmap.feature.forgot_password.navigateToForgotPassword
1616
import com.espressodev.gptmap.feature.info.infoScreen
1717
import com.espressodev.gptmap.feature.info.navigateToInfo
18-
import com.espressodev.gptmap.feature.login.LoginRoute
18+
import com.espressodev.gptmap.feature.login.Login
1919
import com.espressodev.gptmap.feature.login.loginScreen
2020
import com.espressodev.gptmap.feature.login.navigateToLogin
2121
import com.espressodev.gptmap.feature.map.mapScreen
@@ -38,7 +38,7 @@ import com.espressodev.gptmap.feature.verify_auth.verifyAuthScreen
3838
fun GmNavHost(
3939
appState: GmAppState,
4040
modifier: Modifier = Modifier,
41-
startDestination: String = LoginRoute
41+
startDestination: Any = Login
4242
) {
4343
val navController = appState.navController
4444
NavHost(
@@ -52,7 +52,7 @@ fun GmNavHost(
5252
navigateToProfile = navController::navigateToProfile,
5353
navigateToSnapToScript = navController::navigateToSnapToScript,
5454
navigateToGallery = {
55-
appState.navigateToTopLevelDestination(TopLevelDestination.SCREENSHOT_GALLERY)
55+
appState.navigateToTopLevelDestination(TopLevelDestination.SCREENSHOTGALLERY)
5656
}
5757
)
5858
loginScreen(

app/src/main/kotlin/com/espressodev/gptmap/navigation/TopLevelDestination.kt

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ enum class TopLevelDestination(
2020
GmIcons.ExploreOutlined,
2121
AppText.explore
2222
),
23-
SCREENSHOT_GALLERY(
23+
SCREENSHOTGALLERY(
2424
GmIcons.ScreenshotFilled,
2525
GmIcons.ScreenshotOutlined,
2626
AppText.gallery

build-logic/convention/src/main/kotlin/AndroidFeatureConventionPlugin.kt

+2
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
1111
apply("gptmap.android.library")
1212
apply("gptmap.android.hilt")
1313
apply("gptmap.android.detekt")
14+
apply("org.jetbrains.kotlin.plugin.serialization")
1415
}
1516

1617
dependencies {
@@ -26,6 +27,7 @@ class AndroidFeatureConventionPlugin : Plugin<Project> {
2627
implementation(libs.findLibrary("androidx-lifecycle-viewModelCompose").get())
2728
implementation(libs.findLibrary("kotlinx-coroutines-android").get())
2829
implementation(libs.findLibrary("kotlinx-collections-immutable").get())
30+
implementation(libs.findLibrary("kotlinx-serialization-json").get())
2931
}
3032
}
3133
}

build-logic/convention/src/main/kotlin/com/espressodev/gptmap/AndroidCompose.kt

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ internal fun Project.configureAndroidCompose(
3838
androidTestImplementation(libs.findLibrary("androidx-compose-ui-test").get())
3939
androidTestImplementation(libs.findLibrary("androidx-compose-ui-test-junit4").get())
4040
debugImplementation(libs.findLibrary("androidx-compose-ui-testManifest").get())
41+
implementation(libs.findLibrary("androidx-navigation-compose").get())
4142
}
4243
}
4344

core/domain/src/main/kotlin/com/espressodev/gptmap/core/domain/GetNextOrPrevFavouriteUseCase.kt

+4-8
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,8 @@ class GetNextOrPrevFavouriteUseCase @Inject constructor(
3030
}
3131
}
3232

33-
fun Coordinates.haversineDistance(other: Coordinates): Double {
34-
val earthRadiusKm = 6371.0 // Earth's radius in kilometers
33+
internal fun Coordinates.haversineDistance(other: Coordinates): Double {
34+
val earthRadiusKm = 6371.0
3535
val deltaLatRadians = Math.toRadians(other.latitude - this.latitude)
3636
val deltaLonRadians = Math.toRadians(other.longitude - this.longitude)
3737
val sinHalfDeltaLat = sin(deltaLatRadians / 2)
@@ -43,7 +43,7 @@ fun Coordinates.haversineDistance(other: Coordinates): Double {
4343
return earthRadiusKm * c
4444
}
4545

46-
fun Favourite.findClosestFavourites(favourites: List<Favourite>): Pair<Favourite?, Favourite?> {
46+
internal fun Favourite.findClosestFavourites(favourites: List<Favourite>): Pair<Favourite?, Favourite?> {
4747
val currentLocation = content.coordinates
4848
val sortedFavourites =
4949
favourites.sortedBy { currentLocation.haversineDistance(it.content.coordinates) }
@@ -56,10 +56,6 @@ fun Favourite.findClosestFavourites(favourites: List<Favourite>): Pair<Favourite
5656
}
5757
}
5858

59-
for ((direction, group) in directionGroups) {
60-
println("$direction: ${group.joinToString { it.first.content.city }}")
61-
}
62-
6359
val leftFavourite =
6460
directionGroups["RIGHT"]?.firstOrNull { it.first.favouriteId != this.favouriteId }?.first
6561
val rightFavourite =
@@ -72,7 +68,7 @@ enum class Direction {
7268
NORTH_EAST, EAST, SOUTH_EAST, SOUTH, SOUTH_WEST, WEST, NORTH_WEST, NORTH
7369
}
7470

75-
fun Coordinates.directionTo(other: Coordinates): Direction {
71+
internal fun Coordinates.directionTo(other: Coordinates): Direction {
7672
val lat1 = Math.toRadians(other.latitude)
7773
val lat2 = Math.toRadians(this.latitude)
7874
val lon1 = Math.toRadians(other.longitude)

feature/favourite/src/main/kotlin/com/espressodev/gptmap/feature/favourite/FavouriteNavigation.kt

+5-3
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,20 @@ import androidx.navigation.NavController
44
import androidx.navigation.NavGraphBuilder
55
import androidx.navigation.NavOptions
66
import androidx.navigation.compose.composable
7+
import kotlinx.serialization.Serializable
78

8-
const val FavouriteRoute = "favourite_route"
9+
@Serializable
10+
data object Favourite
911

1012
fun NavController.navigateToFavourite(navOptions: NavOptions? = null) {
11-
navigate(FavouriteRoute, navOptions)
13+
navigate(Favourite, navOptions)
1214
}
1315

1416
fun NavGraphBuilder.favouriteScreen(
1517
popUp: () -> Unit,
1618
navigateToMap: (String) -> Unit,
1719
) {
18-
composable(FavouriteRoute) {
20+
composable<Favourite> {
1921
FavouriteRoute(popUp = popUp, navigateToMap = navigateToMap)
2022
}
2123
}

feature/forgot-password/src/main/kotlin/com/espressodev/gptmap/feature/forgot_password/ForgotPasswordNavigation.kt

+6-6
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,17 @@ import androidx.navigation.NavController
44
import androidx.navigation.NavGraphBuilder
55
import androidx.navigation.NavOptions
66
import androidx.navigation.compose.composable
7+
import kotlinx.serialization.Serializable
78

8-
const val ForgotPasswordRoute = "forgot_password_route"
9+
@Serializable
10+
data object ForgotPassword
911

1012
fun NavController.navigateToForgotPassword(navOptions: NavOptions? = null) {
11-
navigate(ForgotPasswordRoute, navOptions)
13+
navigate(ForgotPassword, navOptions)
1214
}
1315

14-
fun NavGraphBuilder.forgotPasswordScreen(
15-
navigateToLogin: () -> Unit,
16-
) {
17-
composable(route = ForgotPasswordRoute) {
16+
fun NavGraphBuilder.forgotPasswordScreen(navigateToLogin: () -> Unit) {
17+
composable<ForgotPassword> {
1818
ForgotPasswordRoute(
1919
clearAndNavigateLogin = navigateToLogin,
2020
)

feature/info/src/main/kotlin/com/espressodev/gptmap/feature/info/InfoNavigation.kt

+5-3
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,17 @@ import androidx.navigation.NavController
44
import androidx.navigation.NavGraphBuilder
55
import androidx.navigation.NavOptions
66
import androidx.navigation.compose.composable
7+
import kotlinx.serialization.Serializable
78

8-
const val InfoRoute = "info_route"
9+
@Serializable
10+
data object Info
911

1012
fun NavController.navigateToInfo(navOptions: NavOptions? = null) {
11-
navigate(InfoRoute, navOptions)
13+
navigate(Info, navOptions)
1214
}
1315

1416
fun NavGraphBuilder.infoScreen(popUp: () -> Unit) {
15-
composable(InfoRoute) {
17+
composable<Info> {
1618
InfoRoute(popUp = popUp)
1719
}
1820
}

feature/login/build.gradle.kts

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ android {
77
namespace = "com.espressodev.gptmap.feature.login"
88
}
99

10-
1110
dependencies {
1211
implementation(projects.core.google)
1312
implementation(libs.play.services.auth)

feature/login/src/main/kotlin/com/espressodev/gptmap/feature/login/LoginNavigation.kt

+5-3
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,26 @@ import androidx.navigation.NavController
44
import androidx.navigation.NavGraphBuilder
55
import androidx.navigation.NavOptionsBuilder
66
import androidx.navigation.compose.composable
7+
import kotlinx.serialization.Serializable
78

8-
const val LoginRoute = "login_route"
9+
@Serializable
10+
data object Login
911

1012
fun NavController.navigateToLogin(
1113
navOptionsBuilder: NavOptionsBuilder.() -> Unit = {
1214
launchSingleTop = true
1315
popUpTo(0) { inclusive = true }
1416
}
1517
) {
16-
navigate(LoginRoute, builder = navOptionsBuilder)
18+
navigate(Login, builder = navOptionsBuilder)
1719
}
1820

1921
fun NavGraphBuilder.loginScreen(
2022
navigateToMap: () -> Unit,
2123
navigateToRegister: () -> Unit,
2224
navigateToForgotPassword: () -> Unit
2325
) {
24-
composable(LoginRoute) {
26+
composable<Login> {
2527
LoginRoute(
2628
navigateToMap = navigateToMap,
2729
navigateToRegister = navigateToRegister,

feature/map/src/main/kotlin/com/espressodev/gptmap/feature/map/MapNavigation.kt

+8-9
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,24 @@ package com.espressodev.gptmap.feature.map
33
import androidx.navigation.NavController
44
import androidx.navigation.NavGraphBuilder
55
import androidx.navigation.NavOptions
6-
import androidx.navigation.NavType
76
import androidx.navigation.compose.composable
8-
import androidx.navigation.navArgument
97
import androidx.navigation.navOptions
8+
import kotlinx.serialization.Serializable
109

11-
const val MapRoute = "map_route"
1210
const val FavouriteId = "favId"
13-
const val MapRouteWithArg = "$MapRoute/{$FavouriteId}"
11+
12+
@Serializable
13+
data class Map(val favId: String = "favId")
14+
fun Map.toDestinationString() = "Map?$favId={$favId}"
15+
1416
fun NavController.navigateToMap(
1517
favouriteId: String = "default",
1618
navOptions: NavOptions? = navOptions {
1719
popUpTo(0) { inclusive = true }
1820
launchSingleTop = true
1921
}
2022
) {
21-
navigate("$MapRoute/$favouriteId", navOptions)
23+
navigate(Map(favouriteId), navOptions)
2224
}
2325

2426
fun NavGraphBuilder.mapScreen(
@@ -28,10 +30,7 @@ fun NavGraphBuilder.mapScreen(
2830
navigateToSnapToScript: (String) -> Unit,
2931
navigateToGallery: () -> Unit
3032
) {
31-
composable(
32-
route = "$MapRoute/{$FavouriteId}",
33-
arguments = listOf(navArgument(FavouriteId) { type = NavType.StringType })
34-
) {
33+
composable<Map> {
3534
MapRoute(
3635
navigateToStreetView = { locPair ->
3736
navigateToStreetView(locPair.first, locPair.second)

feature/profile/src/main/kotlin/com/espressodev/gptmap/feature/profile/ProfileNavigation.kt

+10-6
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,27 @@ import androidx.navigation.NavGraphBuilder
55
import androidx.navigation.NavOptions
66
import androidx.navigation.compose.composable
77
import androidx.navigation.navigation
8+
import kotlinx.serialization.Serializable
89

9-
const val ProfileGraph = "profile_graph"
10-
const val ProfileRoute = "profile_route"
10+
@Serializable
11+
data object Profile
12+
13+
@Serializable
14+
data object ProfileGraph
1115

1216
fun NavController.navigateToProfile(navOptions: NavOptions? = null) {
13-
navigate(ProfileRoute, navOptions)
17+
navigate(Profile, navOptions)
1418
}
1519

1620
fun NavGraphBuilder.profileScreen(
1721
popUp: () -> Unit,
1822
navigateToLogin: () -> Unit,
1923
navigateToInfo: () -> Unit,
2024
navigateToDelete: () -> Unit,
21-
nestedGraphs: NavGraphBuilder.() -> Unit
25+
nestedGraphs: NavGraphBuilder.() -> Unit,
2226
) {
23-
navigation(route = ProfileGraph, startDestination = ProfileRoute) {
24-
composable(ProfileRoute) {
27+
navigation<ProfileGraph>(startDestination = Profile) {
28+
composable<Profile> {
2529
ProfileRoute(
2630
popUp = popUp,
2731
navigateToLogin = navigateToLogin,

0 commit comments

Comments
 (0)