Skip to content

ilittle7/Router

Repository files navigation

Router

Router是一个根据uri来开启一个Activity或者执行一个行为的框架。

特点

  1. 支持通过字符串、Uri或者Intent跳转
  2. 支持多module使用
  3. 支持拦截器,全局拦截器
  4. 支持自定义注解,定义一个拦截器集合
  5. 支持拦截器优先级
  6. 支持kotlin的objectcompanion object作为Interceptor
  7. 拦截器使用方式类似okhttp
  8. 内部使用Intent作为Request
  9. kotlin风格的API,并对java做了兼容
  10. 代码检查和自动修复(Lint)
  11. 支持含有通配符或者前缀匹配

Gradle依赖配置

接下来在工程中的每个使用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
}

API

Router

一个router表示由一个uri指向的Activity或者行为。@Router指定的类都是一个router。

@Router注解可以添加在一个ActivityService或者一个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 的作用是简化写法。

@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。通常拦截器的执行时序如下图:

Interceptor执行时序图

自定义拦截器注解

自定义拦截器注解表示一个拦截器集合,目的是为了增强复用性,下面是一个例子:

@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 : Interceptor

RouterAction

RouterAction可以将一个函数表示为一个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

Response代表了整个route过程的结果。内部的Intent默认情况下就是作为Request的Intent。

在Interceptor和RouterAction实现中都可以干涉默认的返回结果。

开启dialog

下面的例子为一个简单的作为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}")
    }
}

有以下几点需要说明:

  1. Dialog必须为androidx的DialogFragment(或其子类)的子类
  2. routerIntent是一个DialogFragment的扩展属性,用来获取开启当前dialog的Intent。
  3. Router内部会判断启动当前dialog的类型,可以为FragmentActivity或者是已经attach的Fragment对象,若不是会直接返回失败Response

routeForResult函数

routeForResult()表示startActivityForResult()。如果目标是一个RouterAction那么就会运行这个Action的runForResult函数。

判断一个uri是否指向一个router

一般写法:

if(someUri in RouterCollection){
	// ...
}

这里someUri也可以传path或者Intent,用法同route()函数

FallbackRouter

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/**

传递ActivityOptions

下面是安卓原本的传递方式和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()
})

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors