Skip to content

Navigator

张涛(tao281.zhang) edited this page Sep 5, 2022 · 1 revision

页面跳转能力介绍

路由可用于处理页面跳转

  • 支持 ActivityFragment
  • 支持path与页面多对一关系或一对一关系
  • 页面Path支持正则表达式声明
  • 支持 json 格式路由表导出
  • 支持动态下发 json 路由表
  • 支持任意object跨模块传递(无需序列化,且能保证对象类型)
  • 支持页面跳转拦截处理
  • 支持自定义页面参数解析方式(例如将json解析为对象)
  • 支持使用路由跳转到第三方SDK中的Activity(Fragment)

1.0 路由表

1.1 声明路由项

如果一个页面(支持 Activity、Fragment)允许被路由打开,则需要使用注解 @Route 声明路由项,每个页面允许声明多个路由项,也就是一对多的能力,极大降低多端路由统一时的业务影响面。

参数释义

  • path: 路由path 【必传】。
    建议是一个url。path内支持使用正则表达式(为了匹配效率,正则必须包含反双斜杠\),允许多个path对应同一个Activity(Fragment)。
  • action: 自定义事件【可选】。 一般用来打开目标页面后做一个执行动作,例如自定义页面弹出广告弹窗
  • description: 页面描述【可选】。 会被记录到路由表中,方便后期排查的时候知道每个path或Activity是什么业务
  • params: 页面参数【可选】。 自动写入intent中,允许写在路由表中动态下发修改默认值,或通过路由跳转时代码传入。
@Route(path = "http://therouter.com/home", action = "action://scheme.com",
        description = "第二个页面", params = {"hello", "world"})
public class HomeActivity extends AppCompatActivity {
}

1.2 路由表生成规则

如果两条路由的path、目标className完全相同,则认为是同一条路由,不会考虑参数是否相同

路由表生成规则:编译期按照如下顺序取并集
覆盖规则:根据如下顺序,如果相同,后者可以覆盖前者的路由表规则。

  1. 编译期解析注解生成路由表
  2. 首先取 业务模块 aar 中的路由表
  3. 再取 主app module 代码中的路由表
  4. 最后取 assets/RouteMap.json 文件中声明的路由表。
  • 如果编译期没有这个文件,会生成一份默认路由表放在这个目录内;如果有,会将路由表合并
  • 路由表生成时可配置是否启用检查路由合法性,判断目标页面是否存在,(warning/error)级别
  1. 运行时线上动态下发的路由表
  • 路由表允许线上动态下发,将覆盖本地路由表,详见 【1.3 远端下发路由表】

1.3 远端下发路由表

TheRouter项目每次编译后,会在apk内生成一份路由表,默认路径为:/assets/therouter/routeMap.json

同时这份路由表也支持远端动态下发,例如远端可以针对不同的APP版本,下发不同的路由表达到配置目的。因此有两种推荐的方式可供使用方选择:

  1. 将打包系统与配置系统打通,每次新版本APP打包后自动将assets/目录中的配置文件上传到配置系统,下发给对应版本APP 。优点在于全自动不会出错。
  2. 配置系统无法打通,线上手动下发需要修改的路由项,因为 TheRouter 会自动用最新下发的路由项覆盖包内的路由项。优点在于精确,且流量资源占用小。

注:一旦你设置了自定义的InitTask,原框架内路由表初始化任务将不再执行,你需要自己处理找不到路由表时的兜底逻辑,一种建议的处理方式见如下代码。

// 此代码 必须 在 Application.super.onCreate() 之前调用
RouteMap.setInitTask(new RouterMapInitTask() {
    /** 
     * 此方法执行在异步
     */
    @Override
    public void asyncInitRouteMap() {
        // 此处为纯业务逻辑,每家公司远端配置方案可能都不一样
        // 不建议每次都请求网络,否则请求网络的过程中,路由表是空的,可能造成APP无法跳转页面
        // 最好是优先加载本地,然后开异步线程加载远端配置
        String json = Connfig.doHttp("routeMap");
        // 建议加一个判断,如果远端配置拉取失败,使用包内配置做兜底方案,否则可能造成路由表异常
        if (!TextUtils.isEmpty(json)) {
            List<RouteItem> list = new Gson().fromJson(json, new TypeToken<List<RouteItem>>() {
            }.getType());
            // 建议远端下发路由表差异部分,用远端包覆盖本地更合理
            RouteMap.addRouteMap(list);
        } else {
            // 在异步执行TheRouter内部兜底路由表
            initRouteMap()
        }
    }
});

1.4 为第三方库添加路由表

共有两种方式

  1. 配置文件添加:在本地 assets/RouteMap.json 文件中声明第三方页面的路由表。
  2. 代码运行时添加:
/**
 * @params path 路由Path
 * @params className 第三方页面的Activity或Fragment完整类名
 * @params action 要向页面传入的动作,如果没有传空字符串
 * @params description  第三方页面的文档描述
 */
addRouteItem(RouteItem(path, className, action, description))

1.5 编译期配置

路由表中如果声明了一个不存在的Activity,通常是因为旧代码删除时忘记清理路由表了,这里可选报错或警告,提供参考。
可在local.properties配置编译期属性(warning输出日志,error直接报错)

  • 路由表中声明了不存在的Activity检查 CHECK_ROUTE_MAP=warning warning/error可选

2.0 页面跳转使用方式

2.1 路由跳转

传入的参数可以是 String 和8种基本数据类型、也可以是BundleSerializableParcelable对象,跟 Intent 传值规则一致。

同时也支持为本次跳转的 Intent 添加Flag/Uri/ClipData/identifier等业务特殊参数。

// 传入参数可以通过注解 @Autowired 解析成任意类型,如果是对象建议传json
// context 参数如果不传或传 null,会自动使用 application 替换
TheRouter.build("http://therouter.com/home")
        .withInt("key1", 12345678)
        .withString("key2", "参数")
        .withBoolean("key3", false)
        .withSerializable("key4", object)
        .withObject("object", any) // 与上一个方法不同,这个方法可以传递任意对象,但是接收的地方对象类型请保证一致
        .navigation(context);

        // 如果传入 requestCode,默认使用startActivityForResult启动Activity
        .navigation(context, 123);

        // 如果要打开的是fragment,需要使用
        .createFragment();

2.2 延迟路由跳转

延迟跳转主要应用场景有两种:

  • 第一种:初始化时期,如果路由表的量非常巨大时。这种情况在别的路由框架上要么会白屏一段时间,要么直接丢弃这次跳转。在TheRouter中,框架会暂存当前的跳转动作,在路由表初始化完成后立刻执行跳转。
  • 第二种:从Android 8.0开始,Activity 不能在后台启动页面,这对于业务判断造成了很大的影响。由于可能会有前台 Service 的情况,不能单纯以 Activity 生命周期判断前后台。在TheRouter中,框架允许业务自定义前后台规则,如果为后台情况,可以将跳转动作暂存,当进入前台后再恢复跳转。
// 暂存的动作可以有多个,会在恢复时按顺序执行
TheRouter.build("http://therouter.com/home")
        .withInt("key1", 12345678)
        .padding()// 暂存当前跳转动作
        .navigation(context);
        
// 恢复
sendPendingNavigator(); //toplevel方法,无需类名调用,Java请通过NavigatorKt类名调用

2.3 路由参数接收

使用注解接收对象时,必须调用TheRouter.inject(this);建议直接在 Base 中统一调用。
接收有两种形式:

  • 通过注解自动接收,默认支持 String 和8种基本数据类型,也支持自定义对象的解析
  • 通过代码从 intent 中获取
@Route(path = "http://therouter.com/home")
public class HomeActivity extends BaseActivity {
    // 允许解析成8种基本数据类型或对应封装类
    @Autowired
    int key1;

    // String类型可以自动解析,除了String和8种基本类型封装类以外,
    // 其他的Object需要自定义解析规则,自定义方式见下文高级功能介绍 2.3 节内容
    @Autowired
    String key2;

    // 如果没有声明 name,则使用变量名作为 Intent 解析的key
    @Autowired(name = "key3")
    String user;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 使用注解接收对象时,必须调用这一句,建议直接在 Base 中统一调用。
        TheRouter.inject(this);
        // 当然如果不嫌麻烦,也是允许自己手动解析的,但是只能解析成String,因为传的时候都是以String类型传递
        String key1 = activity.getIntent().getStringExtra("key1");
    }
}

2.4 对象参数接收

页面跳转间如果要对象,有两种方式:

  • 第一种:先将对象序列化为json,将json传递以后,自定义解析器反序列化为对象,参考自定义 @Autowired 解析
  • 第二种:直接传递,在直接传递时需要注意对象类型,接收的地方不要出现强转错误 对象参数只能使用注解接收。使用注解接收对象时,必须调用TheRouter.inject(this);建议直接在 Base 中统一调用。
TheRouter.build("http://therouter.com/home")
        .withString("user_info_object", json) // 传递json对象
        .withObject("user_object", any) // 这个方法可以传递任意对象,但是接收的地方对象类型请保证一致
        .navigation(context);
        
@Route(path = "http://therouter.com/home")
public class HomeActivity extends BaseActivity {
    @Autowired
    User user_info_object;

    @Autowired
    User user_object;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // 使用注解接收对象时,必须调用这一句,建议直接在 Base 中统一调用。
        TheRouter.inject(this);
    }
}

2.5 自定义 @Autowired 解析

页面跳转间如果要对象,需要先将对象序列化为json,将json传递以后再反序列化为对象
通过 @Autowired 注解可以自定义将 json 解析为对象注入

TheRouter.addAutowiredParser(new MyAutowiredParser());

具体Parser类定义可参考代码DefaultUrlParser

3.0 路由跳转自定义处理器

框架内置四种自定义处理器可供业务场景定制,用于在路由跳转过程中,以切面的方式统一修改路由落地页参数信息。

3.1 Path 修改器

应用场景:用于修复客户端上路由 path 错误问题。

例如:相对路径转绝对路径,或由于服务端下发的链接无法固定https或http,但客户端代码写死了 https 的 path,就可以用这种方式统一。
必须在 TheRouter.build() 方法调用前添加处理器,否则处理器前的所有path不会被修改

Navigator.addNavigatorPathFixHandle(new NavigatorPathFixHandle() {
    @Override
    public String fix(String path) {
        path = path.replace("http://", "https://");
        if (!path.contains("www.kymjs.com") && path.startsWith("/")) {
            path = "https://www.kymjs.com" + path;
        }
        return path;
    }
    /**
     * 数字越大,优先级越高,
     * 优先级方法通常情况下不需要重写,除非你真的有什么特殊场景会用到
     */
    @Override
    public int getPriority() {
        return 5;
    }
});

3.2 页面替换器

应用场景:需要将某些path指定为新链接的时候使用。 也可以用在修复链接的场景,但是与 path 修改器不同的是,修改器通常是为了解决通用性的问题,替换器只在页面跳转时才会生效,更多是用来解决特性问题。

例如模块化的时候,首页壳模板组件中开发了一个SplashActivity广告组件作为应用的MainActivity,在闪屏广告结束的时候自动跳转业务首页页面。 但是每个业务不同,首页页面的 Path 也不相同,而不希望让每个业务线自己去改这个首页壳模板组件,此时就可以组件中先写占位符https://kymjs.com/splash/to/home,让接入方通过 Path 替换器解决。
必须在 TheRouter.build().navigation() 方法调用前添加处理器,否则处理器前的所有跳转不会被替换

Navigator.addPathReplaceInterceptor(new PathReplaceInterceptor() {
    @Override
    public String replace(String path) {
        if ("https://kymjs.com/splash/to/home".equals(path)) {
            return "https://kymjs.com/business/home";
        }
        return path;
    }
});

3.3 路由替换器

应用场景:常用在未登录不能使用的页面上。例如访问用户钱包页面,在钱包页声明的时候,可以在路由表上声明本页面是需要登录的,在路由跳转过程中,如果落地页是需要登录的,则先替换路由到登录页,同时将原落地页信息作为参数传给登录页,登录流程处理完成后可以继续执行之前的路由操作。

路由替换器的拦截点更靠后,主要用于框架已经从路由表中根据 path 找到路由以后,对找到的路由做操作。

这种逻辑在所有页面跳转前写不太合适,以前的做法通常是在落地页写逻辑判断用户是否具有权限,但其实在路由层完成更合适。
必须在 TheRouter.build().navigation() 方法调用前添加处理器,否则处理器前的所有跳转不会被替换

Navigator.addRouterReplaceInterceptor(new RouterReplaceInterceptor() {
    @Override
    public RouteItem replace(RouteItem routeItem) {
        if (user.age() < 18 && routeItem.getClassName().contains("ChildrenProhibitActivity")) {
            RouteItem target = new RouteItem();
            target.setClassName(HomeActivity.class.getName());
            String[] path = {"https://kymjs.com/too/young"};
            target.setPathArray(path);
            target.setDescription("也可以在这里修改原有路由的参数信息");
            return target;
        }
        return routeItem;
    }
});

3.4 路由AOP拦截器

与前三个处理器不同的点在于,路由的AOP拦截器全局只能有一个。用于实现AOP的能力,在整个TheRouter跳转的过程中,跳转前,目标页是否找到的回调,跳转时,跳转后,都可以做一些自定义的逻辑处理。

使用场景:场景很多,最常用的是可以拦截一些跳转,例如debug页面在生产环境不打开,或定制startActivity跳转方法。

Navigator.setRouterInterceptor(new RouterInterceptor() {
    @Override
    public void process(RouteItem routeItem, InterceptorCallback callback) {
        if (!TheRouter.isDebug() && routeItem.getClassName().equals("com.kymjs.DebugActivity")) {
            return;
        } else {
            // callback 对象也是可以自定义的,看你怎么用了
            callback.onContinue(routeItem);
        }
    }
});

3.5 跳转结果回调

如果使用TheRouter跳转,传入了一个不识别的的path,则不会有任何处理。你也可以定义一个默认的全局回调,来处理跳转情况,如果落地页是 Fragment 则不会回调。
当然,跳转结果的回调不止这一个用途,可以根据业务有自己的处理。
回调也可以单独为某一次跳转设置,navigation()方法有重载可以传入设置。

NavigatorKt.defaultNavigationCallback(new NavigationCallback() {
    // 落地页Activity打开后,执行到onCreate会回调
    @Override
    public void onActivityCreated(@NonNull Navigator navigator, @NonNull Activity activity) {
        super.onActivityCreated(navigator, activity);
    }

    // startActivity执行后会立刻回调
    @Override
    public void onArrival(@NonNull Navigator navigator) {
        super.onArrival(navigator);
    }
    
    // 找到待跳转的落地页时就会回调(startActivity之前)
    @Override
    public void onFound(@NonNull Navigator navigator) {
        super.onFound(navigator);
    }

    // 找不到落地页的时候会回调
    @Override
    public void onLost(@NonNull Navigator navigator) {
        super.onLost(navigator);
    }
});

4.0 其他API

4.1 判断一个 url 是否为路由Path

如果返回为空,表示当前url不是路由表内的path

// kotlin toplevel方法,Java调用请使用RouteMapKt类
matchRouteMap("url填这里") == null