preface

Navigation binding a route is cumbersome and becomes more difficult to maintain as the number of interfaces increases, so we try to hardcode this part into dynamic generation mode.

Demo address: github.com/qdsfdhvh/kr…

content

1. Bind routes

In Compose, Navigation binds a route in the following way:

composable(
    route = "/labs/detail/{id}/{name}? desc={desc}",
    arguments = listOf(
        navArgument("id") { type = NavType.LongType },
        navArgument("name") { type = NavType.StringType },
        navArgument("desc") { type = NavType.StringType; nullable = true)) {},valid = it.arguments!! .get("id") as Long
    valname = it.arguments!! .get("name") as String
    valdesc = it.arguments? .get("desc") as? String
    LabsDetailScene(
        navController = navController,
        id = id,
        name = name,
        desc = desc
    )
}

navController.navigate("/labs/detail/10/balala? desc=xmx")
Copy the code

Route and arguments are cumbersome to configure, so we try to generate them dynamically via KSP.

2. Dynamically generate routes

We configured the project as Kotlin (“multiplatform”) to use the Expect /actual keyword;

Define an @route annotation to configure the Route as follows:

@Route
expect object LabsRoute {
    val Tab: String
    object Detail {
        operator fun invoke(id: String, name: String, detail: String?).: String
    }
}
Copy the code

Use KSP to generate actual implementation:

actual object LabsRoute {
    actual val Tab = "LabsRoute/Tab"
    actual object Detail {
        const val path = "LabsRoute/Detail/{id}/{name}? detail={detail}"
        actual operator fun invoke(id: String, name: String, detail: String?).: String {
            return "LabsRoute/Detail/$id/$name? detail=$detail"}}}Copy the code

Thus, our use becomes the following instead of hard-coding the route.

composable( route = LabsRoute.Detail.path, ... ) {... } navController.navigate(LabsRoute.Detail(10."balala"."xmx"))
Copy the code

3. Change the route to constant

Arguments work the same way, but annotations only support constants, so we need to change the route argument to const;

Const ‘val’ should have an initializer will fail to compile, but Expect /actual supports constants. This is a bug and is ignored by @suppress.

@Suppress("CONST_VAL_WITHOUT_INITIALIZER")
@Route
expect object LabsRoute {
    const val Tab: String
    object Detail {
        operator fun invoke(id: String, name: String, detail: String?).: String
    }
}
Copy the code

4. Dynamically register routes

Define an @NavgraphDestination annotation, like a regular routing framework:

@NavGraphDestination( route = LabsRoute.Detail.path, )
fun LabsDetailScene(
    navController: NavController.@Path("id") id: Long.@Path("name") name: String.@Query("desc") desc: String?).{... }Copy the code

Generate code similar to the above with the auxiliary @path and @Query annotations;

But because we the route is dynamic, KSP may encounter Java at compile time. The util. NoSuchElementException: Collection contains no element matching the predicate. That is, the route is not generated well;

After all, it is a wave of ova operation, so it is expected to encounter this error, we now have to check the route, if the error is randomly return a list to trigger KSP retry:

override fun process(resolver: Resolver): List<KSAnnotated> {
    val symbols = resolver...
    val generatedFunctionSymbols = resolver...

    fun checkValidRoute(symbol: KSFunctionDeclaration): Boolean {
        return try {
            symbol.getAnnotationsByType(NavGraphDestination::class).first().route
            true
        } catch (e: Throwable) {
            false}}if(symbols.any { ! checkValidRoute(it) }) {return (symbols + generatedFunctionSymbols).toList()
    }

    ...
}
Copy the code

5. Collect routes

Also define an @GeneratedFunction annotation and write an empty shell method:

@GeneratedFunction
expect fun NavGraphBuilder.generatedLabsRoute(
    navController: NavController
)
Copy the code

KSP puts the @navgraphDestination function in the current Module into this entry:

actual fun NavGraphBuilder.generatedLabsRoute(navController: NavController) {
    composable(
        route = "/labs/detail/{id}/{name}? desc={desc}",
        arguments = listOf(
            navArgument("id") { type = NavType.LongType },
            navArgument("name") { type = NavType.StringType },
            navArgument("desc") { type = NavType.StringType; nullable = true)) {},valid = it.arguments!! .get("id") as Long
        valname = it.arguments!! .get("name") as String
        valdesc = it.arguments? .get("desc") as? String
        LabsDetailScene(
            navController = navController,
            id = id,
            name = name,
            desc = desc
        )
    }
}
Copy the code

Since the External Module of the Expect function is referable, it can be imported directly into the app or injected through DI:

@Composable
fun Route(a) {
    valnavController = rememberNavController() NavHost(navController, startDestination = ...) {... generatedLabsRoute(navController) } }Copy the code

conclusion

The article doesn’t have much content (a bit watery), and many of the projects probably won’t be easy to switch to kotlin multi-platform, so it’s not very practical;

Here mainly want to share the expect/actual+ KSP combination, dynamically generated code can directly establish contact with the outside, I think this is very playable, looking forward to the big guys later to play some big pattern of operations.