Skip to content

SteffenEckardt/Compose-Navigation-Generator

Repository files navigation

Compose/Navigation/Generator

ℹ️ 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! 🎁

Content

Introduction

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.

Setup

TODO...

Usage

  • 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()
}

Example: Single destination without parameters

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()
}

Example: Multiple destinations without parameters

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()
}

Example: Multiple destinations with parameters

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)
}

Demo app

Coming soon...

Setup

Coming soon...

Used Libraries