diff --git a/README.md b/README.md index b9c85d0..93f4fd0 100644 --- a/README.md +++ b/README.md @@ -6,8 +6,219 @@ Navigation Compose Extended is a complementary library for AndroidX Jetpack Navi improve creation of navigation elements, as destination routes, arguments, deep links, … in a more idiomatic way than using literals. -Visit the [project website](https://sergiobelda.dev/navigation-compose-extended/) for documentation -and API Reference. +Take a look at the [sample-app-annotations](https://github.com/serbelga/navigation-compose-extended/tree/main/sample-app-annotations) and [sample-app](https://github.com/serbelga/navigation-compose-extended/tree/main/sample-app) for working examples. + +Visit the [project website](https://sergiobelda.dev/navigation-compose-extended/) for documentation and API Reference. + +## Download + +```kotlin +dependencies { + // Add AndroidX Navigation Compose dependency. + implementation("androidx.navigation:navigation-compose:$nav_version") + + implementation("dev.sergiobelda.navigation.compose.extended:navigation-compose-extended:$version") + // Use KSP to generate NavDestinations with annotations. + implementation("dev.sergiobelda.navigation.compose.extended:navigation-compose-extended-compiler:$version") + ksp("dev.sergiobelda.navigation.compose.extended:navigation-compose-extended-compiler:$version") +} +``` + +## Usage + +> [!IMPORTANT] +> In the next documentation, annotations are used to create navigation elements, but we can also create them programmatically. + +The `NavDestination` represents some Destination in the Navigation graph. + +### Create a NavDestination + +```kotlin +@NavDestination( + destinationId = "home", + name = "Home", // Optional: NavDestination name. + isTopLevelNavDestination = true, // Optional: Mark NavDestination as a top-level destination. +) +@Composable +fun HomeScreen() {} +``` + +If we use the `@NavDestination` annotation as above, the compiler will generate a `NavDestination` object associated with this destination. + +```kotlin +public object HomeNavDestination : NavDestination() { + override val destinationId: String = "home" +} +``` + +### Using the NavDestination into the NavHost + +```kotlin +NavHost(navController = navController, startNavDestination = HomeNavDestination) { + composable(navDestination = HomeNavDestination) { + HomeScreen() + } + composable(navDestination = SettingsNavDestination) { + SettingsScreen() + } +} +``` + +> [!NOTE] +> Here we are using wrappers (`NavHost`, `composable`) that receive the `NavDestination` type to create the navigation graph. +> Visit the [API Reference](https://sergiobelda.dev/navigation-compose-extended/api/navigation-compose-extended/dev.sergiobelda.navigation.compose.extended/index.html) for +> more information. + +`NavDestination` class also offers variables for `route`, `arguments` and `deepLinks` that can be used as follows if we don't want to use these wrappers: + +```kotlin +NavHost(navController = navController, startDestination = HomeNavDestination.route) { + composable( + route = HomeNavDestination.route, + deepLinks = HomeNavDestination.deepLinks + ) { + HomeScreen() + } + composable( + route = HomeNavDestination.route, + arguments = HomeNavDestination.arguments + ) { + SettingsScreen() + } +} +``` + +### Navigate + +We can navigate to some destination using the actions functions provided by the `NavAction` class. +The `NavAction.navigate()` function receive a `NavRoute` instance to navigate to some destination. +This `NavRoute` associated with a destination can be obtained using the `navRoute()` function in the `NavDestination` class or +the `safeNavRoute()` function if we are using annotation. +In the following code, we navigate to the `SettingsNavDestination`: + +```kotlin +composable(navDestination = HomeNavDestination) { + HomeScreen( + navigateToSettings = { + navAction.navigate( + SettingsNavDestination.navRoute() + ) + }, + ) +} +``` + +### Navigate with arguments + +The `NavArgumentKey` represents a navigation argument key in the Navigation graph. +If we are using annotations, we can annotate function parameters as: + +```kotlin +@NavDestination( + name = "Settings", + destinationId = "settings", +) +@Composable +fun SettingsScreen( + @NavArgument userId: Int, + @NavArgument(defaultValue = "Default") text: String?, // Set default value for the NavArgument. + @NavArgument(name = "custom-name", defaultValue = "true") result: Boolean, // Set a custom NavArgument name. + viewModel: SettingsViewModel +) {} +``` + +The compiler will generate an enum class containing the navigation arguments keys for this navigation destination. + +```kotlin +public enum class SettingsNavArgumentKeys( + override val argumentKey: String, +) : NavArgumentKey { + UserIdNavArgumentKey("userId"), + TextNavArgumentKey("text"), + CustomNameNavArgumentKey("customName"), + ; +} +``` + +and will set the `argumentsMap` property in the `NavDestination` that associate each `NavArgumentKey` with its properties. + +```kotlin +public object SettingsNavDestination : NavDestination() { + override val destinationId: String = "settings" + + override val argumentsMap: Map Unit> = mapOf( + SettingsNavArgumentKeys.UserIdNavArgumentKey to { + type = NavType.IntType + }, + SettingsNavArgumentKeys.TextNavArgumentKey to { + type = NavType.StringType + nullable = true + defaultValue = "Default" + }, + SettingsNavArgumentKeys.CustomNameNavArgumentKey to { + type = NavType.BoolType + defaultValue = true + }, + ) +``` + +If we use annotations, we can use the generated `safeNavRoute()` function with the navigation arguments as parameters: + +```kotlin +composable(navDestination = HomeNavDestination) { + HomeScreen( + navigateToSettings = { + navAction.navigate( + SettingsNavDestination.safeNavRoute( + userId = 1, + text = "Text", + customName = true + ) + ) + }, + ) +} +``` + +otherwise, we must use `navRoute()` function associating the NavArgumentKey to its value. + +```kotlin +composable(navDestination = HomeNavDestination) { + HomeScreen( + navigateToSettings = { + navAction.navigate( + SettingsNavDestination.navRoute( + SettingsNavArgumentKeys.UserIdNavArgumentKey to 1, + SettingsNavArgumentKeys.TextNavArgumentKey to "Text", + SettingsNavArgumentKeys.CustomNameNavArgumentKey to true + ) + ) + }, + ) +} +``` + +### Retrieving the navigation arguments values + +The value of navigation arguments can be obtained using the `NavArgs` class. The `NavDestination.navArgs()` provides an instance +of this class. There are multiple getters to retrieve values: + +```kotlin +composable(navDestination = SettingsNavDestination) { navBackStackEntry -> + val navArgs = SettingsNavDestination.navArgs(navBackStackEntry) + val userId = navArgs.getInt(SettingsNavArgumentKeys.UserIdNavArgumentKey) ?: 0 + SettingsScreen( + userId = userId, +``` + +If we use annotations processor, a `SafeNavArgs` class is generated with getters for each navigation argument: + +```kotlin +composable(navDestination = SettingsNavDestination) { navBackStackEntry -> + val navArgs = SettingsSafeNavArgs(navBackStackEntry) + SettingsScreen( + userId = navArgs.userId ?: 0, +``` ## License diff --git a/docs/index.md b/docs/index.md index 7bf7587..a85c364 100644 --- a/docs/index.md +++ b/docs/index.md @@ -14,6 +14,55 @@ keys, and functions to retrieve arguments values in a more secure way. [![Maven Central](https://img.shields.io/maven-central/v/dev.sergiobelda.navigation.compose.extended/navigation-compose-extended)](https://search.maven.org/search?q=g:dev.sergiobelda.navigation.compose.extended) +```kotlin +dependencies { + // Add AndroidX Navigation Compose dependency. + implementation("androidx.navigation:navigation-compose:$nav_version") + + implementation("dev.sergiobelda.navigation.compose.extended:navigation-compose-extended:$version") + // Use KSP to generate NavDestinations with annotations. + implementation("dev.sergiobelda.navigation.compose.extended:navigation-compose-extended-compiler:$version") + ksp("dev.sergiobelda.navigation.compose.extended:navigation-compose-extended-compiler:$version") +} +``` + +```kotlin +@NavDestination( + name = "Settings", + destinationId = "settings", +) +@Composable +fun SettingsScreen( + @NavArgument userId: Int, + @NavArgument(defaultValue = "Default") text: String?, // Set default value for the NavArgument. + @NavArgument(name = "custom-name", defaultValue = "true") result: Boolean, // Set a custom NavArgument name. +) { +``` + +```kotlin +val navController = rememberNavController() +val navAction = rememberNavAction(navController) +NavHost(navController = navController, startNavDestination = HomeNavDestination) { + composable(navDestination = HomeNavDestination) { + HomeScreen( + navigateToSettings = { userId -> + navAction.navigate( + SettingsNavDestination.safeNavRoute(userId = userId) + ) + }, + ) + } + composable(navDestination = SettingsNavDestination) { navBackStackEntry -> + val safeNavArgs = SettingsSafeNavArgs(navBackStackEntry) + SettingsScreen( + userId = safeNavArgs.userId ?: 0, + text = safeNavArgs.text, + result = safeNavArgs.customName ?: false, + ) + } +} +``` + ## License ``` diff --git a/docs/usage.md b/docs/usage.md index fb27cce..dc98402 100644 --- a/docs/usage.md +++ b/docs/usage.md @@ -1,118 +1,198 @@ -## Create Destinations +> [!IMPORTANT] +> In the next documentation, annotations are used to create navigation elements, but we can also create them programmatically. The `NavDestination` represents some Destination in the Navigation graph. +## Create a NavDestination + ```kotlin -object SearchNavDestination : NavDestination() { - override val destinationId: String = "search" -} +@NavDestination( + destinationId = "home", + name = "Home", // Optional: NavDestination name. + isTopLevelNavDestination = true, // Optional: Mark NavDestination as a top-level destination. +) +@Composable +fun HomeScreen() {} +``` + +If we use the `@NavDestination` annotation as above, the compiler will generate a `NavDestination` object associated with this destination. -object SearchResultNavDestination : NavDestination() { - override val destinationId: String = "searchresult" +```kotlin +public object HomeNavDestination : NavDestination() { + override val destinationId: String = "home" } ``` -Using the `NavDestination` into the NavHost: +## Using the NavDestination into the NavHost ```kotlin -val navController = rememberNavController() -NavHost( - navController = navController, - startDestination = SearchNavDestination.route -) { - composable(route = SearchNavDestination.route) { SearchScreen() } - composable(route = SearchResultNavDestination.route) { SearchResultScreen() } +NavHost(navController = navController, startNavDestination = HomeNavDestination) { + composable(navDestination = HomeNavDestination) { + HomeScreen() + } + composable(navDestination = SettingsNavDestination) { + SettingsScreen() + } } ``` -We can navigate to some destination using the actions functions provided by the `NavAction` class. -The `NavAction.navigate` function receive a `NavRoute` instance to navigate to some destination. -This `NavRoute` associated with a destination can be obtained using the `navRoute()` function in the `NavDestination` class. -In the following code, we navigate to the `SearchResultNavDestination`: +> [!NOTE] +> Here we are using wrappers (`NavHost`, `composable`) that receive the `NavDestination` type to create the navigation graph. +> Visit the [API Reference](https://sergiobelda.dev/navigation-compose-extended/api/navigation-compose-extended/dev.sergiobelda.navigation.compose.extended/index.html) for +> more information. + +`NavDestination` class also offers variables for `route`, `arguments` and `deepLinks` that can be used as follows if we don't want to use these wrappers: ```kotlin -val navController = rememberNavController() -val navAction = rememberNavAction(navController) -NavHost( - navController = navController, - startDestination = SearchNavDestination.route -) { - composable(route = SearchNavDestination.route) { - SearchScreen( - navigateToSearchResult = { - navAction.navigate( - SearchResultNavDestination.navRoute() - ) - } - ) +NavHost(navController = navController, startDestination = HomeNavDestination.route) { + composable( + route = HomeNavDestination.route, + deepLinks = HomeNavDestination.deepLinks + ) { + HomeScreen() + } + composable( + route = HomeNavDestination.route, + arguments = HomeNavDestination.arguments + ) { + SettingsScreen() } } ``` -## Navigate with arguments +## Navigate -The `NavArgumentKey` represents some argument in the Navigation graph. -We can define multiple keys for our destinations. +We can navigate to some destination using the actions functions provided by the `NavAction` class. +The `NavAction.navigate()` function receive a `NavRoute` instance to navigate to some destination. +This `NavRoute` associated with a destination can be obtained using the `navRoute()` function in the `NavDestination` class or +the `safeNavRoute()` function if we are using annotation. +In the following code, we navigate to the `SettingsNavDestination`: ```kotlin -enum class SearchResultNavArgumentKeys(override val argumentKey: String) : NavArgumentKey { - SearchNavArgumentKey("search"), - CategoryNavArgumentKey("category") +composable(navDestination = HomeNavDestination) { + HomeScreen( + navigateToSettings = { + navAction.navigate( + SettingsNavDestination.navRoute() + ) + }, + ) } ``` -Next, we set the `NavArgumentKey` into the `NavDestination` using the `argumentsMap` property. +## Navigate with arguments + +The `NavArgumentKey` represents a navigation argument key in the Navigation graph. +If we are using annotations, we can annotate function parameters as: ```kotlin -object SearchResultNavDestination : NavDestination() { - override val destinationId: String = "searchresult" - - override val argumentsMap: Map Unit> = - mapOf( - SearchResultNavArgumentKeys.SearchNavArgumentKey to { - type = NavType.StringType - }, - SearchResultNavArgumentKeys.CategoryNavArgumentKey to { - type = NavType.StringType - nullable = true - defaultValue = "All" - } - ) +@NavDestination( + name = "Settings", + destinationId = "settings", +) +@Composable +fun SettingsScreen( + @NavArgument userId: Int, + @NavArgument(defaultValue = "Default") text: String?, // Set default value for the NavArgument. + @NavArgument(name = "custom-name", defaultValue = "true") result: Boolean, // Set a custom NavArgument name. + viewModel: SettingsViewModel +) {} +``` + +The compiler will generate an enum class containing the navigation arguments keys for this navigation destination. + +```kotlin +public enum class SettingsNavArgumentKeys( + override val argumentKey: String, +) : NavArgumentKey { + UserIdNavArgumentKey("userId"), + TextNavArgumentKey("text"), + CustomNameNavArgumentKey("customName"), + ; } ``` -The `NavRoute` class will generate automatically the route with the arguments depending on if they -are nullable or have a default value. +and will set the `argumentsMap` property in the `NavDestination` that associate each `NavArgumentKey` with its properties. + +```kotlin +public object SettingsNavDestination : NavDestination() { + override val destinationId: String = "settings" -Arguments can be set to the `NavHost` using the `arguments` property: + override val argumentsMap: Map Unit> = mapOf( + SettingsNavArgumentKeys.UserIdNavArgumentKey to { + type = NavType.IntType + }, + SettingsNavArgumentKeys.TextNavArgumentKey to { + type = NavType.StringType + nullable = true + defaultValue = "Default" + }, + SettingsNavArgumentKeys.CustomNameNavArgumentKey to { + type = NavType.BoolType + defaultValue = true + }, + ) +``` + +If we use annotations, we can use the generated `safeNavRoute()` function with the navigation arguments as parameters: ```kotlin -NavHost { - composable( - route = SearchResultNavDestination.route, - arguments = SearchResultNavDestination.arguments - ) { - ... - } +composable(navDestination = HomeNavDestination) { + HomeScreen( + navigateToSettings = { + navAction.navigate( + SettingsNavDestination.safeNavRoute( + userId = 1, + text = "Text", + customName = true + ) + ) + }, + ) } ``` -The `navRoute()` function in the `NavDestination` class can receive the arguments as parameters. -In the following code, we are navigating to `SearchResultNavDestination` and we set a value for -the `SearchNavArgumentKey` argument. +otherwise, we must use `navRoute()` function associating the NavArgumentKey to its value. ```kotlin -SearchScreen( - navigateToSearchResult = { search, category -> - navAction.navigate( - SearchResultNavDestination.navRoute( - SearchResultNavArgumentKeys.SearchNavArgumentKey to search, +composable(navDestination = HomeNavDestination) { + HomeScreen( + navigateToSettings = { + navAction.navigate( + SettingsNavDestination.navRoute( + SettingsNavArgumentKeys.UserIdNavArgumentKey to 1, + SettingsNavArgumentKeys.TextNavArgumentKey to "Text", + SettingsNavArgumentKeys.CustomNameNavArgumentKey to true + ) ) - ) - } -) + }, + ) +} ``` +## Retrieving the navigation arguments values + +The value of navigation arguments can be obtained using the `NavArgs` class. The `NavDestination.navArgs()` provides an instance +of this class. There are multiple getters to retrieve values: + +```kotlin +composable(navDestination = SettingsNavDestination) { navBackStackEntry -> + val navArgs = SettingsNavDestination.navArgs(navBackStackEntry) + val userId = navArgs.getInt(SettingsNavArgumentKeys.UserIdNavArgumentKey) ?: 0 + SettingsScreen( + userId = userId, +``` + +If we use annotations processor, a `SafeNavArgs` class is generated with getters for each navigation argument: + +```kotlin +composable(navDestination = SettingsNavDestination) { navBackStackEntry -> + val navArgs = SettingsSafeNavArgs(navBackStackEntry) + SettingsScreen( + userId = navArgs.userId ?: 0, +``` + + ## Navigate with Deep Links In the `AndroidManifest.xml`: diff --git a/sample-app-annotations/src/main/kotlin/dev/sergiobelda/navigation/compose/extended/sample/annotations/ui/main/MainActivity.kt b/sample-app-annotations/src/main/kotlin/dev/sergiobelda/navigation/compose/extended/sample/annotations/ui/main/MainActivity.kt index 92ba601..8e2cd98 100644 --- a/sample-app-annotations/src/main/kotlin/dev/sergiobelda/navigation/compose/extended/sample/annotations/ui/main/MainActivity.kt +++ b/sample-app-annotations/src/main/kotlin/dev/sergiobelda/navigation/compose/extended/sample/annotations/ui/main/MainActivity.kt @@ -54,7 +54,7 @@ class MainActivity : ComponentActivity() { SettingsScreen( userId = safeNavArgs.userId ?: 0, text = safeNavArgs.text, - result = safeNavArgs.alternativeResult ?: false, + result = safeNavArgs.customName ?: false, ) } } diff --git a/sample-app-annotations/src/main/kotlin/dev/sergiobelda/navigation/compose/extended/sample/annotations/ui/settings/SettingsScreen.kt b/sample-app-annotations/src/main/kotlin/dev/sergiobelda/navigation/compose/extended/sample/annotations/ui/settings/SettingsScreen.kt index 1cf7017..705016c 100644 --- a/sample-app-annotations/src/main/kotlin/dev/sergiobelda/navigation/compose/extended/sample/annotations/ui/settings/SettingsScreen.kt +++ b/sample-app-annotations/src/main/kotlin/dev/sergiobelda/navigation/compose/extended/sample/annotations/ui/settings/SettingsScreen.kt @@ -32,8 +32,8 @@ import dev.sergiobelda.navigation.compose.extended.compiler.annotation.NavDestin @Composable fun SettingsScreen( @NavArgument userId: Int, - @NavArgument(defaultValue = "Default") text: String?, - @NavArgument(name = "alternative-result", defaultValue = "true") result: Boolean, + @NavArgument(defaultValue = "Default") text: String?, // Set default value for the NavArgument. + @NavArgument(name = "custom-name", defaultValue = "true") result: Boolean, // Set a custom NavArgument name. ) { Box(modifier = Modifier.fillMaxSize()) { Column { diff --git a/sample-app/src/main/java/dev/sergiobelda/navigation/compose/extended/sample/ui/home/HomeScreen.kt b/sample-app/src/main/java/dev/sergiobelda/navigation/compose/extended/sample/ui/home/HomeScreen.kt index 412640a..8bc8983 100644 --- a/sample-app/src/main/java/dev/sergiobelda/navigation/compose/extended/sample/ui/home/HomeScreen.kt +++ b/sample-app/src/main/java/dev/sergiobelda/navigation/compose/extended/sample/ui/home/HomeScreen.kt @@ -56,7 +56,6 @@ import dev.sergiobelda.navigation.compose.extended.sample.ui.search.initial.Sear import dev.sergiobelda.navigation.compose.extended.sample.ui.search.result.SearchResultNavArgumentKeys import dev.sergiobelda.navigation.compose.extended.sample.ui.search.result.SearchResultNavDestination import dev.sergiobelda.navigation.compose.extended.sample.ui.search.result.SearchResultScreen -import dev.sergiobelda.navigation.compose.extended.sample.ui.search.result.customNavRoute import dev.sergiobelda.navigation.compose.extended.sample.ui.yourlibrary.YourLibraryNavDestination import dev.sergiobelda.navigation.compose.extended.sample.ui.yourlibrary.YourLibraryScreen @@ -130,7 +129,9 @@ private fun NavGraphBuilder.searchNavDestination( SearchInitialScreen( navigateToSearchResult = { search, category -> navAction.navigate( - SearchResultNavDestination.customNavRoute(search), + SearchResultNavDestination.navRoute( + SearchResultNavArgumentKeys.SearchNavArgumentKey to search, + ), ) }, ) diff --git a/sample-app/src/main/java/dev/sergiobelda/navigation/compose/extended/sample/ui/search/result/SearchResultNavigation.kt b/sample-app/src/main/java/dev/sergiobelda/navigation/compose/extended/sample/ui/search/result/SearchResultNavigation.kt index 0955f76..3335c02 100644 --- a/sample-app/src/main/java/dev/sergiobelda/navigation/compose/extended/sample/ui/search/result/SearchResultNavigation.kt +++ b/sample-app/src/main/java/dev/sergiobelda/navigation/compose/extended/sample/ui/search/result/SearchResultNavigation.kt @@ -46,9 +46,3 @@ object SearchResultNavDestination : NavDestination( "sample://searchresult", ) } - -fun SearchResultNavDestination.customNavRoute(search: String, category: String? = "Default") = - navRoute( - SearchResultNavArgumentKeys.SearchNavArgumentKey to search, - SearchResultNavArgumentKeys.CategoryNavArgumentKey to category, - )