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

本文介绍了如何使用Kotlin元编程库KSP结合KotlinPoet,自定义注解来简化Jetpack Compose的导航路由配置。通过注解在每个屏幕级别定义路由地址,提供工具类进行参数传递和路由跳转,实现了类似ARouter的导航框架。文章详细阐述了实现思路、注解定义、工具类设计以及生成代码的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在上一篇 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值