在上一篇 Kotlin 元编程之 KSP 全面突破 中,通过几个设计模式相关的例子来生成代码,其逻辑都比较简单,没有涉及到Android相关的API 业务类,而本文的例子会涉及到使用 Android API 相关的代码。
在之前Jetpack Compose中的导航路由一文中,我提到开源库 compose-destination 就是借助 KSP 来生成代码的,如果你去看它的源码,就会发现它是通过纯KSP的方式生成代码的,没有使用KotlinPoet。下面通过 KSP + KotlinPoet 的方式也来自定义实现一个下简单的可以通过注解配置的Compose导航路由框架。
当然,实现思路最终还是要通过Compose原生的导航路由API来实现,只不过我们可以对其进行一些封装,隐藏那些烦人又麻烦的配置操作细节。
具体的思路是这样的:我们只需要要为每个屏幕级别的Composable
添加一个注解,通过该注解配置路由地址,类似ARouter那样,然后提供一个工具类进行路由跳转,并且可以在跳转起始页面和目标页面之间传递任何参数。
例如,现在有四个文件,每个文件中都有一个屏幕的Composable:
Screen01.kt :
@Router(path = "/main/firstScreen/", isStart = true)
@Composable
fun FirstScreen() {
......
}
Screen02.kt :
@Router(path = "/main/secondScreen/")
@Composable
fun SecondScreen() {
......
}
Screen03.kt :
@Router(path = "/main/thirdScreen/")
@Composable
fun ThirdScreen() {
......
}
Screen04.kt :
@Router(path = "/main/fourthScreen/")
@Composable
fun FourthScreen(
@Key(name = "useName") useName: String,
@Key(name = "age") age: Int,
@Key(name = "user") user: Person
) {
......
}
这样就可以了,接下来就是通过一个管理类来根据注解的路径进行跳转。注意到上面的 FourthScreen
的参数也添加了注解,在进行跳转的时候,我们可以传递参数,而在接受页面通过这些注解自动接受参数值。
注解类非常简单:
@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.SOURCE)
annotation class Router(
val path: String,
val isStart: Boolean = false,
)
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.SOURCE)
annotation class Key(val name: String)
这里 Router
注解类中用 isStart = true
来表示是否是启动屏幕,对应 NavHost
组件中的startDestination
参数。
然后定义一个管理工具类RouterManager
,内容如下:
class RouterManager(
private val navController: NavHostController,
private val collectors: Map<IScreen, ParamsCollector>,
private val onNavigateFailed: ((IllegalArgumentException, IScreen) -> Unit)? = null
) {
fun navigateTo(route: IScreen) {
try {
navController.navigate(route.name)
} catch (e : IllegalArgumentException) {
onNavigateFailed?.invoke(e, route)
}
}
fun navigateTo(route: IScreen, withParams: MutableMap<String, Any?>.() -> Unit) {
val map = mutableMapOf<String, Any?>()
map.withParams()
collectors[route]?.apply {
emit(map)
}
navigateTo(route)
}
fun navigateBack(route: IScreen? = null, inclusive: Boolean = false, saveState: Boolean = false) {
if (route == null) {
navController.popBackStack()
} else {
navController.popBackStack(route.name, inclusive, saveState)
}
}
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
fun getParams(route: IScreen): Map<String, Any?> {
val collector = collectors[route]
if (collector != null) {
val state = collector.getStateFlow().collectAsStateWithLifecycle()
return state.value
}
return mapOf()
}
}
val LocalComposeRouter = staticCompositionLocalOf<RouterManager> { error("没有找到RouterManager") }
fun MutableMap<String, Any?>.with(param: Pair<String, Any?>) {
this[param.first] = param.second
}
fun Context.showToast(msg: String) {
Toast.makeText(this, msg, Toast.LENGTH_LONG).show()
}
RouterManager 没有几个方法,就是对 NavHostController
的简单封装,然后对于参数,是通过一个Map来保存的,Map的键是一个IScreen
抽象接口,Map的值是一个ParamsCollector
对象,该对象是参数的载体。
IScreen
抽象接口内容如下:
interface IScreen {
val name : String
}
它只有一个属性,就是路由地址。在最终实现生成代码之后,我们会收集添加@Router
注解的每个屏幕级别的Composable
来生一个密封类,让密封类实现IScreen
抽象接口,密封类的实现子类的名字就是对应的每个屏幕级的Composable
函数名。最终希望以密封类的子类形式提供给路由框架的使用者在进行路由导航地址的时候选择使用。
ParamsCollector
类的实现如下:
class ParamsCollector(
private val flow: MutableStateFlow<Map<String, Any?>> = MutableStateFlow(mapOf())
) {
fun emit(map: Map<String, Any?>) {
flow.value = map
}
fun getStateFlow(): StateFlow<Map<String, Any?>> {
return flow.asStateFlow()
}
}
该类主要是一个StateFlow
的封装,没有什么具体内容。参数形式是通过Flow持有的Map值,并且这里Map的value类型是Any?
, 即我们期望在导航时可以传任意类型的参数。
有了上面的工具类以后,我们期望的最终使用方式如下:
路由跳转:通过 router.navigateTo(SomeScreen)
例如:
@Router(path = "/main/firstScreen/", isStart = true)
@Composable
fun FirstScreen() {
val router = LocalComposeRouter.current
Column {
Button(onClick = { router.navigateTo(Screen.SecondScreen) }) {...}
}
}
路由传参:通过 router.navigateTo(SomeScreen)
后面的 lambda { }
中追加 with(key to value)
的形式
例如:
@Router(path = "/main/secondScreen/")
@Composable
fun SecondScreen() {
val router = LocalComposeRouter.current
Column {
Button(onClick = {
router.navigateTo(Screen.ThirdScreen) {
with("name" to "张三")
with("id" to 123)
with("person" to Person("jack", 23))
}
}) {
......
}
}
}
路由参数接收:我们可以提供两种方式,一种是直接在对应需要接受参数的屏幕级的Composable
函数上添加参数,然后对参数添加类似 @Key(name = "useName")
的注解,导航到该页面时,由框架自动设置参数的值。另一种方式是,用户可以借助框架提供的 API router.getParams()
自己手动解析参数,这样灵活性更好。例如:
@Router(path = "/main/thirdScreen/")
@Composable
fun ThirdScreen() {
val router = LocalComposeRouter.current
val params = router.getParams(Screen.ThirdScreen)
val name : String? = params["name"] as String?
val id : Int = (params["id"] ?: -1) as Int
val person : Person? = params["person"] as Person?
Column {
......
}
}
在这些期望的方式和行为定义好之后,那么我们需要生成的代码应该是什么呢?
需要生成的主要是下面的代码:
import androidx.compose.runtime.*
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.fly.compose.ksp.application.entity.Person
import com.fly.mycompose.test.router.controller.*
sealed class Screen(override val name: String): IScreen {
object First : Screen("FirstScreen")
object Second : Screen("SecondScreen")
object Third : Screen("ThirdScreen")
object Fourth : Screen("FourthScreen")
}
private val collectors: Map<IScreen, ParamsCollector> = mapOf(
Screen.First to ParamsCollector(),
Screen.Second to ParamsCollector(),
Screen.Third to ParamsCollector(),
Screen.Fourth to ParamsCollector()
)
@Composable
fun MainScreen() {
val navController = rememberNavController()
val context = LocalContext.current
val routerManager by remember {
mutableStateOf(
RouterManager(navController, collectors) { _, route ->
context.showToast("路由:${route.name}不存在")
}
)
}
CompositionLocalProvider(LocalComposeRouter provides routerManager) {
NavHost(navController = navController, startDestination = Screen.First.name) {
composable(Screen.First.name) { FirstScreenWrapper() }
composable(Screen.Second.name) { SecondScreenWrapper() }
composable(Screen.Third.name) { ThirdScreenWrapper() }
composable(Screen.Fourth.name) { FourthScreenWrapper() }
}
}
}
@Composable
fun FirstScreenWrapper() {
FirstScreen()
}
@Composable
fun SecondScreenWrapper() {
SecondScreen()
}
@Composable
fun ThirdScreenWrapper() {
ThirdScreen()
}
@Composable
fun FourthScreenWrapper() {
val router = LocalComposeRouter.current
val params = router.getParams(Screen.Fourth)
val useName = params["useName"]
val age = params["age"]
val user = params["user"]
if (useName == null || age == null || user == null) {
val context = LocalContext.current
LaunchedEffect(Unit) {
context.showToast("参数为空, 请检查参数")
router.navigateBack()
}
}
else {
FourthScreen(useName as String, age as Int, user as Person)
}
}
上面代码搭配前面的工具类是可以直接运行的。其实就是Compose原生的路由导航使用方式,只不过这里将导航跳转和传参委托给了RouterManager
对象而已。
此外注意到,在上面代码中,NavHost
中的每个 composable{ }
中配置的是 XXXScreenWrapper()
对象,而不是我们真正的屏幕级的Composable
组件。这里为什么要包装一层呢?答案是为了传参。在包装层中,我们还可以进行参数的检查校验、用户提醒操作等等,如果将来功能要做的更丰富一点,我们可以做拦截器。
这里使用了 CompositionLocalProvider
来提供 RouterManager
对象,在用户最终使用时,跟 LocalContext
的获取方式一样,通过 LocalComposeRouter.current
获取到路由管理对象。
关于参数的获取,实际上是通过RouterManager
拿到 StateFlow
对象再调用其 collectAsStateWithLifecycle()
,在之前 Flow 相关的文章中提到过,使用这个API会更加安全的收集流中的数据。 如需使用它,你需要在项目中添加下面的依赖:
implementation "androidx.lifecycle:lifecycle-runtime-compose:2.6.0-alpha03"
下面的图很好的解释了我要传参的过程,你可以想象为每个屏幕级的Composable上都插了一根管子:
另外需要注意的是,stateFlow
的所有的 collectAsStatexxx
的API都需要在Composable函数中调用,因此在前面 RouterManager
代码中 getParams
方法上添加有 @Composable
注解。
回过头来再分析一下要生成的代码,很明显要收集一个包含特定注解信息的Composable函数集合,根据该集合要生成一个Screen
密封类,一个map
对象,一个配置NavHost
的 MainScreen
Composable
函数,以及根据集合的每个函数生成一个Wrapper函数。
接下来在 Android Studio 中新建一个 java/kotlin library module
作为开发生成 router 代码的 ksp
模块,然后另外新建一个 android library module
模块将前面的 RouterManager
相关的代码放入其中。
按照上一篇文章配置好KSP相关的依赖之后,在app 模块添加模块依赖:
dependencies {
implementation project(':test_router')
implementation project(':router_processor')
ksp project(':router_processor')
}
由于本文实现的功能使用的注解比较少,只是编写一些demo代码,所以注解相关的类直接放到了 ksp
模块中,在实际项目中,注解类模块、管理类模块、ksp模块 三者应该分开来放置。建议按照如下方式来组织它们:
一切准备就绪后,下面开始编写 KSP 的 Processor 代码:
import com.google.devtools.ksp.processing.*
import com.google.devtools.ksp.symbol.*
import com.google.devtools.ksp.validate
import com.google.devtools.ksp.visitor.KSDefaultVisitor
import com.squareup.kotlinpoet.*
import com.squareup.kotlinpoet.MemberName.Companion.member
import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.ksp.toTypeName
import com.squareup.kotlinpoet.ksp.writeTo
import java.util.*
/**
* 应用ksp的模块未配置options参数时,默认的生成包名和文件名
*/
const val DEFAULT_CODE_GEN_PKG = "com.fly.compose.router.screen"
const val DEFAULT_CODE_GEN_FILE = "NavHostMain"
const val PKG_COMPOSE_RUNTIME = "androidx.compose.runtime"
const val PKG_COMPOSE_NAVIGATION = "androidx.navigation.compose"
const val PKG_COMPOSE_PLATFORM = "androidx.compose.ui.platform"
const val PKG_MY_ROUTER = "com.fly.mycompose.test.router.controller"
val rememberNavController = MemberName(PKG_COMPOSE_NAVIGATION, "rememberNavController")
val NavHost = MemberName(PKG_COMPOSE_NAVIGATION, "NavHost")
val remember = MemberName(PKG_COMPOSE_RUNTIME, "remember")
val mutableStateOf = MemberName(PKG_COMPOSE_RUNTIME, "mutableStateOf")
val CompositionLocalProvider = MemberName(PKG_COMPOSE_RUNTIME, "CompositionLocalProvider")
val Composable = ClassName(PKG_COMPOSE_RUNTIME, "Composable")
val LaunchedEffect = MemberName(PKG_COMPOSE_RUNTIME, "LaunchedEffect")
val LocalContext = MemberName(PKG_COMPOSE_PLATFORM, "LocalContext")
val composable = MemberName(PKG_COMPOSE_NAVIGATION, "composable")
val LocalComposeRouter = MemberName(PKG_MY_ROUTER, "LocalComposeRouter")
val showToast = MemberName(PKG_MY_ROUTER, "showToast")
val IScreen = ClassName(PKG_MY_ROUTER, "IScreen")
val ParamsCollector = ClassName(PKG_MY_ROUTER, "ParamsCollector")
val RouterManager = ClassName(PKG_MY_ROUTER, "RouterManager")
class ComposeRouterProcessor(
private val codeGenerator: CodeGenerator,
private val logger: KSPLogger,
private val options: Map<String, String>,
) : SymbolProcessor {
/**
* 需要收集的信息是一个Composable函数列表
*/
data class RouterData(
val composableList: List<ComposableFun>
)
/**
* 每个被注解标注的Composable函数需要收集的信息
*/
data class ComposableFun(
var packageName: String = "", // 包名
var funName: String = "", // 函数名
var routerPath: String = "", // 路由路径
var isStart: Boolean = false, // 是否为启动页
var keys: MutableList<Pair<String, TypeName>> = mutableListOf() // 参数名和参数类型
)
override fun process(resolver: Resolver): List<KSAnnotated> {
val symbols = resolver.getSymbolsWithAnnotation(Router::class.qualifiedName!!)
val ret = symbols.filter { !it.validate() }.toList()
val composableList = mutableListOf<ComposableFun>()
symbols
.filterIsInstance<KSFunctionDeclaration>()
.filter { it.validate() }
.forEach {
val composableFun = ComposableFun()
it.accept(FunctionVisitor(), composableFun)
composableList.add(composableFun)
}
generateRouterCode(RouterData(composableList))
return ret
// return emptyList()
}
inner class FunctionVisitor: KSDefaultVisitor<ComposableFun, Unit>() {
override fun visitFunctionDeclaration(function: KSFunctionDeclaration, data: ComposableFun) {
val ksFile = function.containingFile ?: return
data.packageName = ksFile.packageName.asString()
data.funName = function.simpleName.asString()
// 拿到@Router注解
val routerAnnotation = function.annotations
.filter { it.shortName.asString() == Router::class.simpleName }
.firstOrNull()
routerAnnotation?.let {
it.arguments.forEach { ksValueArgument ->
val name = ksValueArgument.name?.asString()
val value = ksValueArgument.value
when(name) {
"path" -> data.routerPath = value as String
"isStart" -> data.isStart = value as Boolean
}
}
// 解析参数上的注解
function.parameters.forEach { ksValueParameter ->
ksValueParameter.accept(this, data)
}
}
}
override fun visitValueParameter(valueParameter: KSValueParameter, data: ComposableFun) {
// 拿到@Key注解
val keyAnnotation = valueParameter.annotations
.filter { it.shortName.asString() == Key::class.simpleName }
.firstOrNull()
keyAnnotation?.let {
val valueArgument = it.arguments.firstOrNull() // @Key只有一个参数
valueArgument?.apply {
val key = this.value as String
val typeName = valueParameter.type.toTypeName()
data.keys.add(key to typeName)
}
}
}
override fun defaultHandler(node: KSNode, data: ComposableFun) {}
}
private fun generateRouterCode(data: RouterData) {
if (data.composableList.isEmpty()) return
logger.warn("解析结果========================${data}")
val packageName = options["genCodePackageName"] ?: DEFAULT_CODE_GEN_PKG
val fileName = options["genCodeFileName"] ?: DEFAULT_CODE_GEN_FILE
// 开始构造 Screen 密封类
val screenClass = ClassName(packageName, "Screen")
var startScreen = ""
val Screen = TypeSpec.classBuilder("Screen")
.addModifiers(KModifier.SEALED)
.addSuperinterface(IScreen)
.primaryConstructor(
FunSpec.constructorBuilder()
.addParameter("name", String::class)
.build()
)
.addProperty(
PropertySpec.builder("name", String::class)
.addModifiers(KModifier.OVERRIDE)
.initializer("name")
.build()
)
.apply {
data.composableList.forEach { composableFun ->
addType(
TypeSpec.objectBuilder(composableFun.funName)
.superclass(screenClass)
.addSuperclassConstructorParameter("%S", composableFun.routerPath)
.build()
)
if (startScreen == "" && composableFun.isStart) {
startScreen = "Screen.${composableFun.funName}.name"
}
}
}
.build()
// 开始构造顶层成员 collectors
val collectorsType = Map::class.asClassName().parameterizedBy(IScreen, ParamsCollector)
val collectors = PropertySpec.builder("collectors", collectorsType)
.addModifiers(KModifier.PRIVATE)
.initializer(buildCodeBlock {
add("mapOf( ")
data.composableList.forEach { composableFun ->
add("%T.%L to %T(), ", screenClass, composableFun.funName, ParamsCollector)
}
add(" )")
})
.build()
// 开始构造 ScreenWrapper Composable 函数
val screenWrapperFuns = data.composableList.map { composableFun ->
FunSpec.builder("${composableFun.funName}Wrapper")
.addAnnotation(Composable)
.apply {
if (composableFun.keys.isEmpty()) {
addStatement("%M()", MemberName(composableFun.packageName, composableFun.funName))
} else {
addStatement("val router = %M.current", LocalComposeRouter)
addStatement("val params = router.getParams(Screen.${composableFun.funName})")
val checkExps = mutableListOf<String>()
val paramsToPass = mutableListOf<String>()
val types = mutableListOf<TypeName>()
composableFun.keys.forEach { (name, type) ->
addStatement("val $name = params[%S]", name)
if (!type.isNullable) checkExps += "$name == null"
paramsToPass += "$name as %T"
types += type
}
val checkExp = checkExps.joinToString(" || ")
beginControlFlow("if (${checkExp})")
addStatement("val context = %M.current", LocalContext)
beginControlFlow("%M(Unit)", LaunchedEffect)
addStatement("context.%M(%S)", showToast, "参数为空, 请检查参数")
addStatement("router.navigateBack()")
endControlFlow()
endControlFlow()
beginControlFlow("else")
val currentScreen = MemberName(composableFun.packageName, composableFun.funName)
val params = paramsToPass.joinToString(", ")
addStatement("%M(${params})", currentScreen, *types.toTypedArray())
endControlFlow()
}
}
.build()
}
// 开始构造 MainScreen Composable 函数
val mainScreen = FunSpec.builder("NavHostMainScreen")
.addAnnotation(Composable)
.addStatement("val navController = %M()", rememberNavController)
.addStatement("val context = %M.current", LocalContext)
.beginControlFlow("val routerManager by %M", remember)
.addCode(buildCodeBlock {
add("%M(", mutableStateOf)
add("%T(navController, collectors) { _, route ->", RouterManager)
add("context.%M(%P)", showToast, "路由:\${route.name}不存在")
add("}")
add(")")
})
.endControlFlow()
.beginControlFlow("%M(%M provides routerManager)", CompositionLocalProvider, LocalComposeRouter)
.beginControlFlow("%M(navController = navController, startDestination = %L)", NavHost, startScreen)
.apply {
data.composableList.forEachIndexed { i, composableFun ->
addStatement("%M(Screen.${composableFun.funName}.name) { %N() }", composable, screenWrapperFuns[i])
}
}
.endControlFlow()
.endControlFlow()
.build()
val fileSpec = FileSpec.builder(packageName, fileName)
.addImport(PKG_COMPOSE_RUNTIME, "getValue")
.addType(Screen)
.addProperty(collectors)
.apply { screenWrapperFuns.forEach { addFunction(it) } }
.addFunction(mainScreen)
.build()
fileSpec.writeTo(codeGenerator, Dependencies(true))
}
}
编写完成后在 ksp 模块的resources
目录下的配置文件中添加对应的的 Provider
全类路径:
class ProcessorProvider : SymbolProcessorProvider {
override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
return ComposeRouterProcessor(
environment.codeGenerator,
environment.logger,
environment.options
)
}
}
// 放在 resources/META-INF/services/com.google.devtools.ksp.processing.SymbolProcessorProvider 中
com.fly.compose.ksp.router.processor.ProcessorProvider
完成后build一下项目,在build目录下查看生成的代码:
生成的 NavHostMain.kt
文件完整内容如下:
package com.fly.compose.ksp.application
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
import com.fly.compose.ksp.application.entity.Person
import com.fly.compose.ksp.application.screens.FirstScreen
import com.fly.compose.ksp.application.screens.FourthScreen
import com.fly.compose.ksp.application.screens.SecondScreen
import com.fly.compose.ksp.application.screens.ThirdScreen
import com.fly.mycompose.test.router.controller.IScreen
import com.fly.mycompose.test.router.controller.LocalComposeRouter
import com.fly.mycompose.test.router.controller.ParamsCollector
import com.fly.mycompose.test.router.controller.RouterManager
import com.fly.mycompose.test.router.controller.showToast
import kotlin.Int
import kotlin.String
import kotlin.Unit
import kotlin.collections.Map
public sealed class Screen(
public override val name: String,
) : IScreen {
public object FirstScreen : Screen("/main/firstScreen/")
public object SecondScreen : Screen("/main/secondScreen/")
public object ThirdScreen : Screen("/main/thirdScreen/")
public object FourthScreen : Screen("/main/fourthScreen/")
}
private val collectors: Map<IScreen, ParamsCollector> = mapOf( Screen.FirstScreen to
ParamsCollector(), Screen.SecondScreen to ParamsCollector(), Screen.ThirdScreen to
ParamsCollector(), Screen.FourthScreen to ParamsCollector(), )
@Composable
public fun FirstScreenWrapper(): Unit {
FirstScreen()
}
@Composable
public fun SecondScreenWrapper(): Unit {
SecondScreen()
}
@Composable
public fun ThirdScreenWrapper(): Unit {
ThirdScreen()
}
@Composable
public fun FourthScreenWrapper(): Unit {
val router = LocalComposeRouter.current
val params = router.getParams(Screen.FourthScreen)
val useName = params["useName"]
val age = params["age"]
val user = params["user"]
if (useName == null || age == null || user == null) {
val context = LocalContext.current
LaunchedEffect(Unit) {
context.showToast("参数为空, 请检查参数")
router.navigateBack()
}
}
else {
FourthScreen(useName as String, age as Int, user as Person)
}
}
@Composable
public fun NavHostMainScreen(): Unit {
val navController = rememberNavController()
val context = LocalContext.current
val routerManager by remember {
mutableStateOf(RouterManager(navController, collectors) { _,
route ->context.showToast("""路由:${route.name}不存在""")})}
CompositionLocalProvider(LocalComposeRouter provides routerManager) {
NavHost(navController = navController, startDestination = Screen.FirstScreen.name) {
composable(Screen.FirstScreen.name) { FirstScreenWrapper() }
composable(Screen.SecondScreen.name) { SecondScreenWrapper() }
composable(Screen.ThirdScreen.name) { ThirdScreenWrapper() }
composable(Screen.FourthScreen.name) { FourthScreenWrapper() }
}
}
}
可以看到,生成代码跟前面期望的代码基本一样,虽然有的地方格式换行有点问题,不过这不重要。
注意,KSP 支持在 gradle 中向 SymbolProcessorEnvironment.options
中的传递参数选项,方式如下:
ksp {
arg("option1", "value1")
arg("option2", "value2")
...
}
因此在上面 Processor 代码中生成文件名和包名是通过在 app 模块的 build.gradle 中配置的:
ksp {
arg("genCodePackageName", android.defaultConfig.applicationId)
arg("genCodeFileName", "NavHostMain")
}
但是假如用户没有配置,我们也会在代码中设置一个默认值。
在生成完代码后,就可以在最开始提到的四个屏幕级组件中调用相关的代码:
// Screen01.kt
@Router(path = "/main/firstScreen/", isStart = true)
@Composable
fun FirstScreen() {
val router = LocalComposeRouter.current
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(text = "这是第一页", fontSize = 16.sp)
Button(onClick = { router.navigateTo(Screen.SecondScreen) }) {
Text("跳转到第二页", fontSize = 16.sp)
}
}
}
// Screen02.kt
@Router(path = "/main/secondScreen/")
@Composable
fun SecondScreen() {
val router = LocalComposeRouter.current
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(text = "这是第二页", fontSize = 16.sp)
Button(onClick = {
router.navigateTo(Screen.ThirdScreen) {
with("name" to "张三")
with("id" to 123)
with("person" to Person("jack", 23))
}
}) {
Text("跳转到第三页", fontSize = 16.sp)
}
}
}
// Screen03.kt
@Router(path = "/main/thirdScreen/")
@Composable
fun ThirdScreen() {
val router = LocalComposeRouter.current
val params = router.getParams(Screen.ThirdScreen)
val name : String? = params["name"] as String?
val id : Int = (params["id"] ?: -1) as Int
val person : Person? = params["person"] as Person?
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("这是第三页", fontSize = 16.sp)
Text("接受到的 name:${name} ", fontSize = 16.sp)
Text("接受到的 id:${id} ", fontSize = 16.sp)
Text("接受到的 person:${person} ", fontSize = 16.sp)
Button(onClick = {
router.navigateTo(Screen.FourthScreen) {
with("useName" to "小明")
with("age" to 456)
with("user" to Person("tom", 33))
}
}) {
Text("跳转到第四页", fontSize = 16.sp)
}
}
}
// Screen04.kt
@Router(path = "/main/fourthScreen/")
@Composable
fun FourthScreen(
@Key(name = "useName") useName: String,
@Key(name = "age") age: Int,
@Key(name = "user") user: Person
) {
val router = LocalComposeRouter.current
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text("这是第四页", fontSize = 16.sp)
Text("接受到的 useName:${useName} ", fontSize = 16.sp)
Text("接受到的 age:${age} ", fontSize = 16.sp)
Text("接受到的 user:${user} ", fontSize = 16.sp)
Button(onClick = { router.navigateBack(Screen.FirstScreen)}) {
Text("返回到第一页", fontSize = 16.sp)
}
}
}
在生成的 NavHostMain.kt
文件中,包含NavHost
的Composable
组件是 NavHostMainScreen()
,这是一个固定的名称。我们在 Activity
中使用 NavHostMainScreen()
来 setContent
:
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
KSPApplicationTheme {
Surface(Modifier.fillMaxSize(), color = MaterialTheme.colors.background) {
NavHostMainScreen()
}
}
}
}
}
运行项目,查看效果:
可以看到页面正常跳转,并且参数也正常传递了,而且不管是基本数据类型,还是Class对象类型都可以传。
下面修改一下第三个屏幕对应的代码,将要传给第四个屏幕的参数都去掉:
// Screen03.kt
@Router(path = "/main/thirdScreen/")
@Composable
fun ThirdScreen() {
val router = LocalComposeRouter.current
......
Column(...) {
......
Button(onClick = {
router.navigateTo(Screen.FourthScreen)
}) {
Text("跳转到第四页", fontSize = 16.sp)
}
}
}
再次运行项目,查看效果:
可以看到,从第三页跳转到第四页的时候,没有成功,并且给出了友好提示 “参数为空, 请检查参数
” ,达到了我们的预期。
本文实现的功能比较简单,但是已经足够满足大多数情况的使用,要想达到生产项目中的实用库,还需要经过完善和大量的测试。Compose导航路由中还有其他的功能,如深度链接DeepLink、路由动画等等,当然,有了 KSP + KotlinPoet 的元编程的科技手段,要实现这些功能也只是时间问题。