DSL在路由导航中的应用
路由导航,说白了就是帮你在页面之间跳来跳去的那套机制。Android原生有Intent,但用起来嘛……我个人觉得有点啰嗦。尤其是大型项目,几十个Activity互相跳转,光写那些字符串常量就够你受的。
今天我们来聊聊,怎么用Kotlin DSL把ARouter和ActivityRouter这类框架,封装得更优雅、更安全。
为什么需要DSL封装路由?
先说说痛点。我在项目中遇到过这样的场景:一个模块有20多个页面,每个页面跳转都要写:
ARouter.getInstance()
.build("/user/profile")
.withString("userId", "12345")
.withInt("age", 28)
.withBoolean("isVip", true)
.navigation()
嗯,看起来还行对吧?但问题是:
- 路径字符串写错了,编译不报错,运行时才崩
- 参数类型不匹配,也是运行时才知道
- 每个页面都要记参数名,团队协作容易出错
说白了,就是类型不安全。你想想看,一个字符串"userId"写错了,IDE不会提醒你,测试也不一定能覆盖到,上线后用户点一下就崩了。这种事我经历过不止一次。
类型安全的路由构建
我们先从最基础的开始。假设我们有一个用户详情页,需要传userId和isVip两个参数。传统写法是这样的:
// 传统方式
ARouter.getInstance()
.build("/user/detail")
.withString("userId", userId)
.withBoolean("isVip", isVip)
.navigation()
用DSL封装后,可以写成这样:
// DSL方式
navigate {
to(UserDetailRoute)
params {
userId = "12345"
isVip = true
}
}
是不是清爽多了?而且UserDetailRoute是一个类型安全的对象,不是字符串。怎么做到的呢?
定义路由DSL
我们先定义一个路由描述类:
data class RouteDescriptor(
val path: String,
val params: Map<String, Any?> = emptyMap()
)
然后定义每个页面的路由对象:
object UserDetailRoute : RouteDescriptor(
path = "/user/detail"
) {
var userId: String by Delegates.notNull()
var isVip: Boolean by Delegates.notNull()
}
这里用到了委托属性。为什么?因为我想让参数在编译期就确定类型。你传一个Int给userId?编译器直接报错。
接下来是DSL构建器:
class RouteBuilder {
private var descriptor: RouteDescriptor? = null
private val params = mutableMapOf<String, Any?>()
fun to(route: RouteDescriptor) {
descriptor = route
}
fun params(block: ParamsBuilder.() -> Unit) {
val builder = ParamsBuilder()
builder.block()
params.putAll(builder.params)
}
fun build(): RouteDescriptor {
return descriptor?.copy(params = params)
?: throw IllegalStateException("Route not specified")
}
}
class ParamsBuilder {
val params = mutableMapOf<String, Any?>()
infix fun String.to(value: Any?) {
params[this] = value
}
}
嗯,这里有个小技巧。我用了infix关键字,让参数赋值看起来更自然:
params {
"userId" to "12345"
"isVip" to true
}
但说实话,我个人更推荐用属性委托的方式,因为类型更安全。上面那个to写法,value是Any?,传什么类型都行,容易出错。
参数传递的DSL封装
参数传递是路由中最容易出问题的环节。我记得有一次,后端返回的userId从String变成了Int,前端没适配,结果跳转时传了个Int进去,页面直接崩了。
为了避免这种问题,我们可以用密封类来定义参数类型:
sealed class RouteParam<out T> {
data class StringParam(val value: String) : RouteParam<String>()
data class IntParam(val value: Int) : RouteParam<Int>()
data class BooleanParam(val value: Boolean) : RouteParam<Boolean>()
data class LongParam(val value: Long) : RouteParam<Long>()
data class SerializableParam(val value: java.io.Serializable) : RouteParam<java.io.Serializable>()
}
然后在路由描述类中,用泛型约束参数类型:
class TypedRouteDescriptor<T : RouteParam<*>>(
val path: String,
val param: T? = null
)
这样,你在构建路由时,编译器就能检查参数类型是否匹配:
// 编译通过
val route = TypedRouteDescriptor(
path = "/user/detail",
param = RouteParam.StringParam("12345")
)
// 编译报错!类型不匹配
val route = TypedRouteDescriptor(
path = "/user/detail",
param = RouteParam.IntParam(12345) // 期望String,给了Int
)
拦截器的DSL配置
拦截器是路由中很实用的功能。比如登录检查、埋点统计、权限验证等。传统写法是这样的:
ARouter.getInstance()
.build("/order/confirm")
.withString("orderId", orderId)
.navigation(this, object : NavigationCallback {
override fun onFound(postcard: Postcard?) {
// 路由找到
}
override fun onLost(postcard: Postcard?) {
// 路由丢失
}
override fun onArrival(postcard: Postcard?) {
// 到达目标
}
override fun onInterrupt(postcard: Postcard?) {
// 被拦截
}
})
用DSL封装后:
navigate {
to(OrderConfirmRoute)
params {
orderId = "ORD2024001"
}
interceptors {
+LoginInterceptor() // 登录检查
+AnalyticsInterceptor() // 埋点统计
+PermissionInterceptor() // 权限验证
}
onArrival {
// 到达后的回调
}
onInterrupt {
// 被拦截后的处理
}
}
这里用到了+操作符重载,让拦截器列表看起来更简洁:
class InterceptorBuilder {
private val list = mutableListOf<Interceptor>()
operator fun Interceptor.unaryPlus() {
list.add(this)
}
fun build(): List<Interceptor> = list.toList()
}
为什么用+?我个人觉得这样写起来很顺手,有点像在列表里添加元素的感觉。当然你也可以用add方法,看团队习惯。
完整的DSL路由示例
把上面这些整合起来,一个完整的DSL路由调用大概是这样的:
navigate {
to(UserDetailRoute)
params {
userId = "u_10086"
isVip = true
score = 98
}
interceptors {
+LoginInterceptor()
+VipCheckInterceptor()
}
onArrival { postcard ->
log("到达用户详情页: ${postcard.path}")
}
onInterrupt { postcard ->
showToast("路由被拦截: ${postcard.interruptMessage}")
}
}
对应的DSL构建器实现:
class SafeRouteBuilder {
private var target: TypedRouteDescriptor<*>? = null
private val params = mutableMapOf<String, Any?>()
private val interceptorList = mutableListOf<Interceptor>()
private var onArrivalCallback: ((Postcard) -> Unit)? = null
private var onInterruptCallback: ((Postcard) -> Unit)? = null
fun to(descriptor: TypedRouteDescriptor<*>) {
target = descriptor
}
fun params(block: ParamsScope.() -> Unit) {
val scope = ParamsScope()
scope.block()
params.putAll(scope.params)
}
fun interceptors(block: InterceptorScope.() -> Unit) {
val scope = InterceptorScope()
scope.block()
interceptorList.addAll(scope.interceptors)
}
fun onArrival(callback: (Postcard) -> Unit) {
onArrivalCallback = callback
}
fun onInterrupt(callback: (Postcard) -> Unit) {
onInterruptCallback = callback
}
fun navigate(context: Context) {
val route = target ?: throw IllegalStateException("路由目标未设置")
// 实际调用ARouter或ActivityRouter
ARouter.getInstance()
.build(route.path)
.apply {
params.forEach { (key, value) ->
when (value) {
is String -> withString(key, value)
is Int -> withInt(key, value)
is Boolean -> withBoolean(key, value)
is Long -> withLong(key, value)
else -> withObject(key, value)
}
}
}
.navigation(context, object : NavigationCallback {
override fun onArrival(postcard: Postcard?) {
onArrivalCallback?.invoke(postcard!!)
}
override fun onInterrupt(postcard: Postcard?) {
onInterruptCallback?.invoke(postcard!!)
}
})
}
}
知识体系总览
下面这张图展示了DSL路由导航的核心结构:
避坑指南
最后分享几个我踩过的坑:
- 参数默认值问题:我曾经在DSL中给参数设置了默认值,结果某个页面改了默认值,其他页面没同步更新,导致数据错乱。建议所有参数都显式传递,不要依赖默认值。
- 拦截器顺序:拦截器的执行顺序很重要。比如登录检查应该在权限验证之前。我建议用
orderedInterceptors方法,显式指定优先级。 - 模块间路由解耦:如果模块A要跳转模块B的页面,不要在A中直接引用B的路由类。可以用接口+服务发现的方式,让路由注册中心统一管理。
好了,关于DSL在路由导航中的应用,就聊到这里。下一节我们会继续深入DSL的其他实战场景。
公众号:蓝海资料掘金营,微信deep3321