查看原文
其他

Kroute:基于 KSP 动态生成 Navigation 路由

AndroidPub 2022-07-13

作者:Seiko
https://juejin.cn/post/7069359416969003015

前言

Navigation 绑定一个路由比较麻烦,随着界面的增多后会变得比较难维护,所以我们就尝试把这部分硬编码改成动态生成的方式。我们基于 KSP 实现了一个路由框架 Kroute,并且可以在 Kotlin 跨平台项目中使用,本文介绍一下框架原理及使用方式。

项目地址:https://github.com/qdsfdhvh/kroute

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

routearguments 配置起来都很麻烦,所以我们就尝试通过 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 组合,动态生成的代码可以直接和外部建立联系,这个我觉得可玩性还是很大的,期待大佬们后面能玩出一些大格局的操作。


END

推荐文章 


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存