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.