Kotlin 元编程之 KSP 实战:通过自定义注解配置Compose导航路由

news2025/1/16 22:03:45

在上一篇 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对象,一个配置NavHostMainScreen 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 文件中,包含NavHostComposable组件是 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 的元编程的科技手段,要实现这些功能也只是时间问题。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/171134.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【MySQL进阶】MySQL事务隔离与锁机制底层原理万字总结(建议收藏!!)

【MySQL进阶】MySQL事务隔离与锁机制底层原理万字总结&#xff08;建议收藏&#xff01;&#xff01;&#xff09; 参考资料&#xff1a; 美团技术团队&#xff1a;Innodb中事务隔离级别和锁的关系 数据库的锁&#xff0c;到底锁的是什么&#xff1f; 阿里面试&#xff1a;说说…

来给博客除草了:Learned Indexes for a Google-scale Disk-based Database

1. 引言 这是一篇业界发表在NeurlIPS 2020的Wip论文《Google规模的基于磁盘的数据库的学习索引》。自从学习索引祖师爷Tim KraskaMIT在SIGMOD 2018发表了第一篇learned index的工作之后&#xff0c;有关学习索引的paper呈现 increasing trend。目前&#xff0c;较多的工作focu…

Java基础漏洞(四)

&#xff08;1&#xff09;递归调用的内存 我们定义一个类A&#xff0c;在类A中定义方法test来验证递归。在程序中&#xff0c;当条件符合的时候会不断的递归&#xff0c;只有遇到终止条件时才不再递归&#xff0c;然后便会执行递归语句之下的语句。 如以下程序&#xff1a;当…

高效节能 | 智慧灯杆综合管理解决方案

智慧灯杆的应用越来越广泛&#xff0c;园区、景区、道路照明&#xff0c;三站一场等户外场所大规模铺设路灯&#xff0c;通过智慧物联平台远程控制&#xff0c;可以实现高效节能和智能化运维管理。以解决传统运维管理中&#xff0c;不同路线路灯设备控制不统一&#xff0c;设备…

Yjs解析

本文引用自 摸鱼wiki 1. YATA算法解析 YATA算法是Yjs库的核心冲突解决算法&#xff0c;这里主要讲解下这个算法成立的三个规则&#xff1a; 规则1&#xff1a;禁止发生冲突的插入操作之间的origin连线&#xff08;红线&#xff09;发生交叉。 如上图所示&#xff0c;图中展示…

IDEA创建SpringBoot的Web项目,并使用外部Tomcat

IDEA创建SpringBoot的Web项目&#xff0c;并使用外部Tomcat 目录IDEA创建SpringBoot的Web项目&#xff0c;并使用外部Tomcat一、创建工程二、使用外部Tomcat一、创建工程 1.首先新建一个springboot的war工程&#xff0c;并且将外部的Tomcat整合进我们的idea中&#xff0c;我们…

GW1NSR-LV4CQN48GC6/I5 FPGA呼吸灯

环境 pwm.v $ cat pwm.v module Pwm( input clk, input [31:0] period, input [31:0] duty, output pwmout ); reg [31:0] counter; reg r_pwmout; always (posedge clk) begin if(counter < duty) begin r_pwmout 1; end…

裸机与RTOS到FreeRTOS基础 | FreeRTOS一

目录 一、裸机与RTOS 1.1、裸机 1.2、RTOS 二、FreeRTOS简介 2.1、介绍 2.2、特点 2.3、如何查找相关资料 三、FreeROTS基础 3.1、任务调度 3.2、任务状态 一、裸机与RTOS 1.1、裸机 不带操作系统&#xff0c;又称为前后台系统&#xff0c;前台系统指中断服务函数&a…

docker搭建Openldapphpldapadmin

1&#xff1a;拉取镜像 docker pull osixia/openldap:1.5.0 docker pull osixia/phpldapadmin:0.9.0 2: 编写docker-compose.yaml cd /data/docker/ldap vi docker-compose.yaml docker-compose.yaml内容如下&#xff1a; version: 3.1 services:openldap:image: osixia/o…

python入门常用知识

文章目录前言一、定义二、Tips1.常用方法2. 常用modulestatistics 统计分析常用模块SciPy 算法及统计分析库总结前言 本文用来记录在使用python时一些基础的定义、基础用法或Tips。 一、定义 多变量定义&#xff1a; a, b, c 0, 0, 0数组定义&#xff1a; # 变量定义 a …

CANN 2022 | 开放升级,跃升新台阶

每一位开发者&#xff0c;都是改变世界的力量每一位开发者&#xff0c;都在创造一往无前的AI时代每一位开发者&#xff0c;都在2022砥砺前行&#xff0c;期待在2023再露锋芒逆光而行&#xff0c;只为拂晓回忆2022&#xff0c;哪些年度关键事件让你记忆犹新&#xff1f;是披星戴…

聊聊红黑树,B/B+树和键树

RB树 RB树和AVL树类似&#xff0c;是一种自平衡式的平衡二叉搜索树&#xff0c;AVL不是保证平衡因子不能超过1&#xff0c;红黑的话没有这个要求&#xff0c;他的结点非黑即红&#xff0c;可以达到Logn的查找&#xff0c;插入&#xff0c;删除 RB树的五条性质&#xff1a; 1…

【markdown】语法 添加`emoji`表情

&#x1f4bb; 【markdown】语法添加emoji表情&#x1f3e0;专栏&#xff1a;其他 &#x1f440;个人主页&#xff1a;繁星学编程&#x1f341; &#x1f9d1;个人简介&#xff1a;一个不断提高自我的平凡人&#x1f680; &#x1f50a;分享方向&#xff1a;目前主攻前端&#…

与资源限制有关的题目

文章目录0、资源限制技巧汇总1、题目一&#xff1a;40亿个数&#xff0c;内存限制为1G&#xff0c;如何找到出现次数最多的数2、题目二&#xff1a;40亿个数&#xff0c;内存限制为10MB&#xff0c;找到所有未出现过的数进阶问题1&#xff1a;40亿个数&#xff0c;内存限制为3K…

【计算机体系结构-02】机器模型

1. 机器模型 (Machine Model) 计算机的基本工作就是进行运算&#xff0c;那么计算就需要有用来处理计算方法的处理单元和提供或保存数值的存储单元。一般将用来处理计算方法的处理单元称为 算术逻辑单元 (ALU--Arithmetic Logic Unit)。在一个计算过程中可能会是这样的一个流程…

springboot整合flowable

自动部署 流程图提前画好&#xff0c;放入 指定文件夹 配置&#xff1a; spring.datasource.usernameroot spring.datasource.passwordroot # nullCatalogMeansCurrenttrue 自动生成表 spring.datasource.urljdbc:mysql:///flowable_process?serverTimezoneUTC&nullCat…

CISP_VULHUB_HACK ME PLEASE

vulhub_HACK ME PLEASE简介扫描开放端口探测web服务搜集seedms的信息登录mysql&#xff0c;尝试获得seeddms的密码利用RCE漏洞提权简介 靶机链接&#xff1a;https://www.vulnhub.com/entry/hack-me-please-1,731/难度&#xff1a;简单描述&#xff1a;一个完全为OSCP设计的简…

数据结构之经典八大排序的实现(万字详谈)

文章目录前言1.插入排序2.希尔排序3.选择排序4.堆排序5.冒泡排序6.快速排序hoare方式版本快排实现非递归方式实现快排挖坑法实现快排前后指针法(双指针法&#xff09;快排的各种优化1.减少后几层的递归调用(小区间优化&#xff09;2.三数取中优化3.三路划分(处理大量重复数据&a…

信息论复习—信息的度量

目录 离散信源信息的度量&#xff1a; 离散信源的信息量&#xff1a; 单符号离散无记忆信源&#xff1a; 离散无记忆信源及熵&#xff1a; 自信息&#xff1a; 信息量的定义&#xff1a; 信息熵的定义&#xff1a; 熵的性质&#xff1a; 离散信源的最大熵定理&#xf…

Elasticsearch高级查询—— 关键字精确查询文档

目录一、初始化文档数据二、字段匹配查询文档2.1、概述2.2、示例一、初始化文档数据 在 Postman 中&#xff0c;向 ES 服务器发 POST 请求 &#xff1a;http://localhost:9200/user/_doc/1&#xff0c;请求体内容为&#xff1a; {"name":"张三","age&…