Android路由框架Router分析

Android路由框架Router

什么是路由?说简单点就是映射页面跳转关系的,当然它也包含跳转相关的一切功能。

路由框架的意义

Android系统已经给我们提供了api来做页面跳转,比如startActivity,为什么还需要路由框架呢?我们来简单分析下路由框架存在的意义:

  1. 在一些复杂的业务场景下,灵活性比较强,很多功能都是动态配置的,比如下发一个活动页面,我们事先并不知道具体的目标页面,但如果事先做了约定,提前做好页面映射,便可以自由配置。
  2. 随着业务量的增长,客户端必然随之膨胀,开发人员的工作量越来越大,比如64K问题,比如协作开发问题。App一般都会走向组件化的道路,而组件化的前提就是解耦,那么我们首先要做的就是解耦页面之间的依赖关系。
  3. 简化代码。数行跳转代码精简成一行代码。
  4. 其他...

特性

Router有哪些特性或者优点呢?

  1. 基于注解,使用方便,源码简洁
  2. 链式调用,api友好
  3. 多路径支持
  4. 结果回调,每次跳转都会回调跳转结果
  5. 编译期处理注解,不影响运行时性能
  6. 除了可以使用注解定义路由,还支持手动分配路由
  7. 自定义拦截器,可以对路由进行拦截,比如登录判断和埋点处理
  8. 自定义路由匹配规则,相比较其他路由框架,该项目并没有限制路由的写法,除了内置的几个匹配器,用户完全可以定义自己的规则
  9. 支持隐式Intent跳转
  10. 支持多模块使用,支持组件化开发
  11. 不仅支持注解Activity,还支持注解Fragment
  12. 支持Kotlin

其他功能正在添加中...

集成

router-gradle-plugin router router-compiler
最新版本
[站外图片上传中……(3)]

集成过程也可参考项目主页README

  1. 在项目级的build.gradle中加入依赖:
buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.x ↑'
        classpath 'com.chenenyu.router:gradle-plugin:latest.integration'
    }
}

注意,Router需要使用2.2.0及以上版本的Android gradle plugin来处理注解,截至写作时,最新版本为2.3.3

  1. 在module级的build.gradle中使用plugin:
apply plugin: 'com.android.application/library'
apply plugin: 'com.chenenyu.router'

至此,集成工作就完成了,简单的两步:添加插件路径和应用插件。这应该是类似框架中最简单的集成方式了。

使用

  1. Router需要初始化,用于初始化路由表,建议放到Application中做:
public class App extends MultiDexApplication {
    @Override
    public void onCreate() {
        super.onCreate();
        // 开启log,要放到前面才能看到初始化过程的log
        if (BuildConfig.DEBUG) {
            Router.openLog();
        }
        // 初始化
        Router.initialize(this);
    }
}
  1. 添加路由注解
// 单路径注解
@Route("test")
public class TestActivity extends Activity {
  ...
}

// 多路径注解,这几个注解都能打开该Activity
@Route({"user", "example://user", "http://example.com/user"})
public class UserActivity extends Activity {
  ...
}

@Route("fragment")
public class TestFragment extends Fragment {
  ...
}
  1. 发起路由操作
// 最简单的路由跳转,打开TestActivity
Router.build("test").go(context);

// 其他部分api
Router.build("test")
   .requestCode(int) // 调用startActivityForResult
   .with(bundle)  // 携带跳转参数
   .addFlags(flag)  // 添加标记,比如intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
   .anim(enter, exit)  // 添加跳转动画
   .callback(calback)  // 跳转结果回调
   .go(this);

全部可用API可参考IRouter接口。

  1. startActivityForResult

Router.build(uri).requestCode(int).go(this); 其中requestCode一定得是一个非负数。


go()有如上几个重载方法,如果是在Fragment中发起startActivityForResult路由操作,切记传当前fragment实例哦!不然在fragment中是接受不到回调的。

  1. 添加跳转动画
// 1. 基础动画
Router.build(uri).anim(enter, exit).go(this);
// 2. 转场动画ActivityOptions
Router.build(uri).activityOptions(options).go(this);
  1. 获取目标页面
// 1. 获取intent,然后可以操作intent
Intent intent = Router.build(uri).getIntent(context);
// 2. 获取fragment,然后可以将该fragment添加到Activity中
Fragment fragment = (Fragment) Router.build(uri).getFragment(context);
  1. 全局拦截器
// 1. 添加全局拦截器
Router.addGlobalInterceptor(routeInterceptor);
// 2. 跳过全局拦截器
Router.build(uri).skipInterceptors().go(this);
  1. 添加拦截器

拦截器是Router的功能之一,作用就是在执行路由之前判断是否需要拦截该次路由请求。比如可以在拦截器中做登录状态判断。

// 定义拦截器(通过注解)
@Interceptor("SampleInterceptor")
public class SampleInterceptor implements RouteInterceptor {
    @Override
    public boolean intercept(Context context, @NonNull Uri uri, @Nullable Bundle extras) {
      // 返回true表示拦截当前路由  
      return true;
    }
}
// 定义拦截器(通过代码)
Router.handleInterceptorTable(new InterceptorTable() {
    @Override
    public void handle(Map<String, Class<? extends RouteInterceptor>> map) {
        map.put("SampleInterceptor", SampleInterceptor.class);
    }
});

// 应用拦截器(通过注解)
@Route(value = "test", interceptors = "SampleInterceptor")
public class TestActivity extends AppCompatActivity {
    ...
}
// 应用拦截器(通过代码)
Router.handleTargetInterceptors(new TargetInterceptors() {
    @Override
    public void handle(Map<Class<?>, String[]> map) {
        map.put(TestActivity.class, new String[]{"SampleInterceptor"});
    }
});
  1. 自定义路由

除了可以使用注解来添加路由外(上面步骤2介绍的方式),还可以通过代码手动控制路由表。

// 动态添加路由表
Router.handleRouteTable(new RouteTable() {
    @Override
    public void handle(Map<String, Class<?>> map) {
        map.put("dynamic1", TestActivity.class);
        map.put("dynamic2", TestFragment.class);
        ...
    }
});

该方式与注解没有冲突,可以同时使用。

  1. 路由匹配规则

该功能是Router最大的特色功能,不同于其他框架,Router并没有规定路由的写法规则,而是抽象出Matcher的概念,交给用户去控制。但是Router仍然内置了4个常用的Matcher

MatcherRegistry

匹配优先级从高到低依次是DirectMatcherSchemeMatcherImplicitMatcherBrowserMatcher,关于原理将在后序的原理篇中进行讲解。
假设有如下路由页面:

@Route({"user", "http://example.com/user"})
public class UserActivity extends Activity {
    ...
}

除了可以通过Router.build("test").go(this)Router.build("http://example.com/user").go(this)打开UserActivity之外(DirectMatcher命中),还可以通过Router.build("http://example.com/user?id=9527&status=0").go(this)打开(通过SchemeMatcher命中),并且自动帮你配置了Bundle参数,即:

@Route({"user", "http://example.com/user"})
public class UserActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        Bundle bundle = getIntent().getExtras();
        String id = bundle.getString("id");
        String status = bundle.getString("status");
    }
}

Matcher支持配置多个,会根据优先级依次进行匹配。

  1. 参数注入

上面讲了可以通过路由传递参数,然后在目标页面通过Bundle获取,其实这个过程也被Router简化了。通过@InjectParam注解可以为Activity或者Fragment的成员变量添加参数注入

@Route({"test", "http://example.com/test", "router://test"})
public class TestActivity extends AppCompatActivity {
    @InjectParam
    int id = 123;
    @InjectParam(key = "status")
    private String sts = "default"; // 不建议使用private修饰符

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        Router.injectParams(this); // 实现参数注入
    }
}
  • @InjectParam会在Bundle中取出对应key的值传给成员变量,默认key为变量名,也可以通过key=""属性指定
  • 参数注入支持变量的默认值,目前支持默认值的变量类型有基本数据类型,String,CharSequence,其他类型的默认值都是null
  • 变量不建议使用private修饰符,因为私有的变量会采用反射的方式注入参数
  • 需要使用Router.injectParams(this)来实现最终的参数注入

Router还有其他一些人性化的小功能,在这里就不一一介绍了,有问题可以在项目主页提issue。后续会给大家讲解一下背后的实现原理。

总结

Router是一个十分小巧灵活的路由框架,代码设计也很优雅简洁,且完美支持组件化开发,目前仍在不断地迭代中,源码地址为https://github.com/chenenyu/Router,欢迎各位试用点评。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容