Router是一个根据uri来开启一个Activity或者执行一个行为的框架。
- 支持通过字符串、
Uri或者Intent跳转 - 支持多module使用
- 支持拦截器,全局拦截器
- 支持自定义注解,定义一个拦截器集合
- 支持拦截器优先级
- 支持kotlin的
object和companion object作为Interceptor - 拦截器使用方式类似okhttp
- 内部使用
Intent作为Request - kotlin风格的API,并对java做了兼容
- 代码检查和自动修复(Lint)
- 支持含有通配符或者前缀匹配
接下来在工程中的每个使用Router的module的build.gradle里面添加如下依赖:
dependencies {
implementation 'io.github.ilittle7:router-lib:2.2.1'
implementation 'io.github.ilittle7:router-annotation:2.2.1'
ksp 'io.github.ilittle7:router-processor:2.2.1'
}在顶层的build.gradle中,添加ksp插件:
注意:KSP 版本的前一部分必须与 build 中使用的 Kotlin 版本一致。例如,如果您使用的是 Kotlin 2.0.21,则 KSP 版本必须是 2.0.21-x.y.z 版本之一。
plugins {
id 'com.google.devtools.ksp' version '2.0.21-1.0.27' apply false
}一个router表示由一个uri指向的Activity或者行为。@Router指定的类都是一个router。
@Router注解可以添加在一个Activity、Service或者一个RouterAction上面:
@Router(["a://b/c"])
class DemoActivity: Activity()当开启这个DemoActivity时就可以使用route方法:
route("a://b/c")java 中:
Routers.route(context, "a://b/c");这里也尊重传统的打开Activity的方式,如下写法和用uri的方式等效:
route(Intent(context, DemoActivity::class.java))@RouterBaseUri 的作用是简化写法。
@RouterBaseUri("a://b/")
class MyApplication : Application()这时route方法就可以这么写:
route("/some_path")并且@Router注解也可以直接写path
@Router(["/some_path"])
class DemoActivity: AppCompatActivity()注意:
@RouterBaseUri在所有module中最多只能写一个,所以加在Application上即可。
拦截器的作用是在跳转一个router之前或之后添加一些行为。下面是一个弹出土司的例子:
object ToastInterceptor : Interceptor {
override fun onIntercept(chain: Chain): Response {
Toast.makeText(
chain.launcherWrapper.context,
"ToastInterceptor: ${chain.requestIntent}",
Toast.LENGTH_LONG
).show()
return chain.proceed(chain.requestIntent)
}
}应用ToastInterceptor:
@Router(["/demo"], [ToastInterceptor::class])
class DemoActivity: AppCompatActivity()拦截器的执行顺序可以由@Priority来决定。优先级数字越大的拦截器越早被调用,不加默认为0。通常拦截器的执行时序如下图:
自定义拦截器注解表示一个拦截器集合,目的是为了增强复用性,下面是一个例子:
@Retention(AnnotationRetention.SOURCE)
@Target(AnnotationTarget.CLASS)
@Interceptors([LoginInterceptor::class])
annotation class LoginRequired应用@LoginRequired:
@LoginRequired
@Router("/demo", [ToastInterceptor::class])
class DemoActivity: AppCompatActivity()全局拦截器表示影响所有router的拦截器,包括不同module中的router。用@GlobalInterceptor指定:
@GlobalInterceptor
@Priority(10)
object SomeInterceptor : InterceptorRouterAction可以将一个函数表示为一个router:
@Router(["/toastAction"])
class ToastRouterAction : RouterAction {
override fun run(launcherWrapper: LauncherWrapper, intent: Intent): Response {
Toast.makeText(launcherWrapper.context, "toast by ToastAction", Toast.LENGTH_SHORT).show()
return Response(success = true, intent = intent)
}
}Response代表了整个route过程的结果。内部的Intent默认情况下就是作为Request的Intent。
在Interceptor和RouterAction实现中都可以干涉默认的返回结果。
下面的例子为一个简单的作为Router的dialog:
@Router(["/dialog/test"])
class TestDialogFragment : DialogFragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
return inflater.inflate(R.layout.dialog_test, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val routerIntent = routerIntent
view.findViewById<TextView>(R.id.dialog_text).append(" ${routerIntent?.data}")
}
}有以下几点需要说明:
- Dialog必须为androidx的DialogFragment(或其子类)的子类
routerIntent是一个DialogFragment的扩展属性,用来获取开启当前dialog的Intent。- Router内部会判断启动当前dialog的类型,可以为FragmentActivity或者是已经attach的Fragment对象,若不是会直接返回失败Response
routeForResult()表示startActivityForResult()。如果目标是一个RouterAction那么就会运行这个Action的runForResult函数。
一般写法:
if(someUri in RouterCollection){
// ...
}这里someUri也可以传path或者Intent,用法同route()函数
FallbackRouter是在所有router都不匹配所给的uri的时候执行的一个默认router。它执行startActivity()方法,目的是route(Intent(context, NotRouterActivity::class.java))也可以打开相应Activity。
FallbackRouter本身并不是一个公开的API,但可以给它添加拦截器:
@FallbackInterceptor
class SomeInterceptor: Interceptor路径通配符的一般用法如下:
@Router("/profile/{userId}")
class UserActivity: Activity()此时如下写法就可以开启UserActivity:
route("/profile/114514")此外在UserActivity内部还可以通过如下方式拿到userId:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val userId:String = intent["userId"] ?: ""
}路径通配符还支持前缀匹配,例如"/a/**"可以匹配第一节path为"/a"的任意path。
前缀匹配优先级低于更“详细”的router path声明。例如"/a/b/**"会优先匹配"/a/b/c"
此外可以在router对象里通过intent.postSegments扩展属性拿到被匹配的路径后半部分
优先级:具体路径 > 通配符路径 > 前缀匹配
冲突:具有相同路径判定的path被视为path冲突,会在编译期抛出异常。下面几对path都是冲突的:
/a-/a/a/{b}-/a/{c}/{a}/**-/{b}/**
假设app内被声明的path有如下几个:
/a/b/a/b/c/a/b/**/a/{p1}/a/{p2}/c/a/**
那么下面列出的运行时传入的path的匹配结果如下:
/a/b->/a/b/a/b/c->/a/b/c/a/c->/a/{p1}/a/c/c->/a/{p2}/c/a/b/d->/a/b/**/a/c/d->/a/**
下面是安卓原本的传递方式和router传递方式的对比:
// 传统方式
val aoc: ActivityOptionsCompat = ...
context.startActivity(Intent(context, TargetActivity::class.java), aoc.toBundle())
// Router方式,需要把options放到Intent里面
// 这样做是为了可以在拦截器中控制options
val ao: ActivityOptionsCompat = ...
context.route(Intent(context, TransitionActivity::class.java).apply {
activityOptions = ao.toBundle()
})