KRoute
前言
Navigation绑定一个路由比较麻烦,随着界面的增多后会变得比较难维护,所以我们就尝试把这部分硬编码改成动态生成的方式。
内容
1.绑定路由
在Compose中,Navigation绑定一个路由的方式大致为:
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 },
)
) {
val id = it.arguments!!.get("id") as Long
val name = it.arguments!!.get("name") as String
val desc = it.arguments?.get("desc") as? String
LabsDetailScene(
navController = navController,
id = id,
name = name,
desc = desc
)
}
navController.navigate("/labs/detail/10/balala?desc=xmx")
route和arguments配置起来都很麻烦,所以我们就尝试通过 ksp 去动态生成。
2.动态生成路由
我们把项目配置成了kotlin("multiplatform"),以此来使用expect/actual关键字;
定义一个@Route注解,通过下面的方式来配置路由:
@Route
expect object LabsRoute {
val Tab: String
object Detail {
operator fun invoke(id: String, name: String, detail: String?): String
}
}
使用 ksp 来生成actual实现:
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"
}
}
}
这样,我们的使用就变成了下面这样,不用再硬编码route了。
composable(
route = LabsRoute.Detail.path,
...
) {
...
}
navController.navigate(LabsRoute.Detail(10, "balala", "xmx"))
3.将路由改为常量
处理arguments的思路也差不多,但是注解只支持常量,所以我们需要把route的参数改为const;
直接添加const会提示Const 'val' should have an initializer导致无法编译,不过expect/actual是支持常量的,这个警告更像是个 bug,通过@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
}
}
4.动态注册路由
定义一个@NavGraphDestination注解,像常规的路由框架那样:
@NavGraphDestination(
route = LabsRoute.Detail.path,
)
fun LabsDetailScene(
navController: NavController,
@Path("id") id: Long,
@Path("name") name: String,
@Query("desc") desc: String?
) {
...
}
依靠辅助的@Path和@Query注解,生成和上面差不多的代码;
不过因为我们的route是动态的,ksp 在编译的时候可能会碰到java.util.NoSuchElementException: Collection contains no element matching the predicate.就也就是路由还没生成好的情况;
毕竟是一波套娃的操作,遇到这个错误也是预料之中,我们这里现在的处理是,先检查一遍route,错误的时候随便返回一个 list 触发 ksp 的重试:
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()
}
...
}
5.收集路由
同样定义一个@GeneratedFunction注解,写个空壳方法:
@GeneratedFunction
expect fun NavGraphBuilder.generatedLabsRoute(
navController: NavController
)
ksp 会把当前 module 中的@NavGraphDestination函数到放入这个入口:
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 },
)
) {
val id = it.arguments!!.get("id") as Long
val name = it.arguments!!.get("name") as String
val desc = it.arguments?.get("desc") as? String
LabsDetailScene(
navController = navController,
id = id,
name = name,
desc = desc
)
}
}
由于expect函数外部 module 是可以引用的,可以直接在 app 中导入或者通过 di 注入:
@Composable
fun Route() {
val navController = rememberNavController()
NavHost(navController, startDestination = ...) {
...
generatedLabsRoute(navController)
}
}
结语
文章并没有多少内容(有点水),很多项目估计也不方便切换到 kotlin 多平台,所以不太实用;
这里主要还是想分享下expect/actual+ksp组合,动态生成的代码可以直接和外部建立联系,这个我觉得可玩性还是很大的,期待大佬们后面能玩出一些大格局的操作。
