ℹ️ This library is still under development. The core function is stable, however much more validation will be implemented to increase robustnes and reliability. Also more features will be coming soon! 🎁
Compose/Navigation/Generator | C/N/G is a Kotlin Symbol Processing (KSP) based library which automatically generates navigation support for Jetpack Compose for Android. Unlike the old XML-based navigation graphs, the new Navigation for Compose does not automatically generate naviagtion functions for the destination within and between graphs. Generally, navigation for Jetpack Compose is defined and managed only by code, which currently has to be written from scratch by the developer for each project.
C/N/G uses Annotations and Kotlin Symbol Processing to generate a SetupNavHost
function, as well as a Navigator
class that can be automatically injected into destination composables and used to trigger navigation events.
TODO...
- All composable functions annotated with
@Destination
will be reachable via navigation - The Home-Destination, required by compose-navigation, can be annotated with
@Home
- Within the setup of the composable scope (usually within the ComposeActivity), call the generated
SetupNavHost
function - Use the generated functions fo the
Navigator
class to navigate to the Destinations, home and up. - All navigation functions can take a trailing
NavBuilder.()
to manually control the transition. - In case of syntax errors in your code, stubs for the setup function and navigator class are generated to maintain compatibility and not cause a circuar-error-dependency. As soon as the errors are fixed, the generattor will regenerate the actual code.
// Annotate a destination you want to be able to navigate to with @Destination
@Destination
@Home
@Composable
fun HomeScreen(navigator: Navigator) {
Column(modifier = Modifier.fillMaxSize()) {
Text("Hello World")
/*...*/
}
}
/** GENERATED */
// A setup function for compose-navigaiton is generated, using your destinations
@Composable
public fun SetupNavHost(navHostController: NavHostController): Unit {
NavHost(navController = navHostController, startDestination = "HomeScreen")
{
composable("HomeScreen") {
HomeScreen(navigator=Navigator(navHostController))
}
}
}
/** GENERATED */
// A class of navigation functions is generated
public class Navigator(private val navHostController: NavHostController) {
/** GENERATED from @Destination */
public fun navigateToHomeScreen(navOptions: (NavOptionsBuilder.() -> Unit)? = null): Unit {
navHostController.navigate("HomeScreen", navOptions ?: { })
}
/** GENERATED from @Home */
public fun navigateHome(navOptions: (NavOptionsBuilder.() -> Unit)? = null): Unit {
navHostController.navigate("HomeScreen", navOptions ?: { })
}
/** GENERATED by default */
public fun navigateUp(): Unit {
navHostController.navigateUp()
}
}
// The destination can be navigated to from every composable function, using the NavHostController
@Composable
fun SomeComposableFunction(navController: NavHostController) {
/*...*/
navController.navigateToHomeScreen()
}
A single destination without parameters. The function is annotated with @Destination
which all navigable compose functions must be. Also, compose expects one destination (per nav-graph), to be the home destination which is the default when the nav-graph is initialized. The @Home
destination must be used on a single @Destination
function to inform C/N/G to treat it as the home-destination.
Source
@Destination
@Home
@Composable
fun HomeScreen() {
Column(modifier = Modifier.fillMaxSize()) {
Text("Hello World")
/*...*/
}
}
The SetupNavHost
function itself is a composable function and should be called during initialization of the composeable scope. Usualy this is done during setup of the parent compose activity. The function takes a NavHostController
as parameter, which can also be passed on to composables, if required.
The Navigator#navigateToDestination(...)
function is created for every destination. Additionally, a Navigator#navigateHome(...)
and Navigator#navigateUp(...)
function is generated.
Generated
/** GENERATED */
@Composable
public fun SetupNavHost(navHostController: NavHostController): Unit {
NavHost(navController = navHostController, startDestination = "HomeScreen")
{
composable("HomeScreen") {
HomeScreen(navigator = Navigator(navHostController))
}
}
}
private const val TAG: String = "NavHost" // If enabled, a logging tag is inserted
/** GENERATED */
public class Navigator(private val navHostController: NavHostController) {
/** GENERATED */
public fun navigateToHomeScreen(navOptions: (NavOptionsBuilder.() -> Unit)? = null): Unit {
navHostController.navigate("HomeScreen", navOptions ?: { })
}
/** GENERATED */
public fun navigateHome(navOptions: (NavOptionsBuilder.() -> Unit)? = null): Unit {
navHostController.navigate("HomeScreen", navOptions ?: { })
}
/** GENERATED */
public fun navigateUp(): Unit {
navHostController.navigateUp()
}
}
Usage
@Composable
fun HomeScreen(navigator: Navigator) {
/*...*/
navigator.navigateToDetailScreen()
}
Source
@Destination
@Home
@Composable
fun HomeScreen(navigator: Navigator) {
/*...*/
}
@Destination
@Composable
fun DetailScreen(navigator: Navigator) {
/*...*/
}
Generated
/** GENERATED */
@Composable
public fun SetupNavHost(navHostController: NavHostController): Unit {
NavHost(navController = navHostController, startDestination = "HomeScreen")
{
composable("DetailScreen") {
DetailScreen(navigator = Navigator(navHostController))
}
composable("HomeScreen") {
HomeScreen(navigator = Navigator(navHostController))
}
}
}
private const val TAG: String = "NavHost"
/** GENERATED */
public class Navigator(
private val navHostController: NavHostController,
) {
public fun navigateToDetailScreen(navOptions: (NavOptionsBuilder.() -> Unit)? = null): Unit {
navHostController.navigate("DetailScreen", navOptions ?: { })
}
public fun navigateToHomeScreen(navOptions: (NavOptionsBuilder.() -> Unit)? = null): Unit {
navHostController.navigate("HomeScreen", navOptions ?: { })
}
public fun navigateHome(navOptions: (NavOptionsBuilder.() -> Unit)? = null): Unit {
navHostController.navigate("HomeScreen", navOptions ?: { })
}
public fun navigateUp(): Unit {
navHostController.navigateUp()
}
}
Usage
@Composable
fun HomeScreen(navigator: Navigator) {
// ...
navigator.navigateToDetailScreen()
}
@Composable
fun DetailScreen(navigator: Navigator) {
// ...
navigator.navigateUp()
}
C/N/G supports nullable- and non-nullable parameters as navigation arguments.
If a destination has no parameters, a "simple" route will be generated:
val navigationPath = "DestinationName" // ...
If a destination only useses non-nullable parameters, a "non-nullable" navigation path will be generated:
val navigationPath = "DestinationName/{arg1}/{arg2}/{arg3}" // ...
If one or more of the arguments are nullable, a "nullable" navigation path will be generated:
val navigationPath = "DestinationName?arg1={arg1}&arg2={arg2}&arg3={arg3}" // ...
Source
@Destination
@Home
@Composable
fun HomeScreen() {
/*...*/
}
@Destination
@Composable
fun DetailScreen(name: String, age: Int) {
/*...*/
}
@Destination
@Composable
fun UltraDetailScreen(name: String, age: Int, height: Double? = 1.90) {
/*...*/
}
Generated
/** GENERATED */
@Composable
public fun SetupNavHost(navHostController: NavHostController): Unit {
NavHost(navController = navHostController, startDestination = "HomeScreen") {
// Destination with exclusivly non-nullable arguments
composable("DetailScreen/{argName}/{argAge}",
// Type and properties of navArgs is automatically determined
arguments = listOf(
navArgument("argName") {
nullable = false
type = NavType.StringType
},
navArgument("argAge") {
nullable = false
type = NavType.IntType
},
)) { backStackEntry ->
// Read arguments from backstack
val argName = backStackEntry.arguments?.getString("argName")
val argAge = backStackEntry.arguments?.getInt("argAge")
// Non-null is required for such parameters
requireNotNull(argName)
requireNotNull(argAge)
// Destination is called with provided parameters
DetailScreen(navigator = Navigator(navHostController), name = argName, age = argAge)
}
// Destination with nullable and non-nullable arguments
composable("UltraDetailScreen?argName={argName}&argAge={argAge}&argHeight={argHeight}",
// Type and properties of navArgs is automatically determined
arguments = listOf(
navArgument("argName") {
nullable = false
type = NavType.StringType
},
navArgument("argAge") {
nullable = false
type = NavType.IntType
},
navArgument("argHeight") {
nullable = true
type = NavType.FloatType
},
)) { backStackEntry ->
// Read arguments from backstack
val argName = backStackEntry.arguments?.getString("argName")
val argAge = backStackEntry.arguments?.getInt("argAge")
val argHeight = backStackEntry.arguments?.getDouble("argHeight")
// Non-null is required for such parameters
requireNotNull(argName)
requireNotNull(argAge)
// Destination is called with provided parameters
UltraDetailScreen(navigator = Navigator(navHostController), name = argName, age = argAge, height = argHeight)
}
// Simple, no-argument destination
composable("HomeScreen") {
HomeScreen(navigator = Navigator(navHostController))
}
}
}
/** GENERATED */
public class Navigator(
private val navHostController: NavHostController,
) {
public fun navigateToDetailScreen(
name: String,
age: Int,
navOptions: (NavOptionsBuilder.() -> Unit)? = null,
): Unit {
navHostController.navigate("DetailScreen/$name/$age", navOptions ?: { })
}
public fun navigateToUltraDetailScreen(
name: String,
age: Int,
height: Double?,
navOptions: (NavOptionsBuilder.() -> Unit)? = null,
): Unit {
navHostController.navigate("UltraDetailScreen?argName=$name&argAge=$age&argHeight=$height", navOptions ?: { })
}
public fun navigateToHomeScreen(navOptions: (NavOptionsBuilder.() -> Unit)? = null): Unit {
navHostController.navigate("HomeScreen", navOptions ?: { })
}
public fun navigateHome(navOptions: (NavOptionsBuilder.() -> Unit)? = null): Unit {
navHostController.navigate("HomeScreen", navOptions ?: { })
}
public fun navigateUp(): Unit {
navHostController.navigateUp()
}
}
Usage
@Composable
fun SomeComposableFunction(navigator: Navigator) {
navigator.navigateToHomeScreen()
navigator.navigateToDetailScreen(name = "Steffen", age = 27)
navigator.navigateToUltraDetailScreen(name = "Steffen", age = 27, height = null)
}
Coming soon...
Coming soon...