Android 客户端路由框架的整理和思考

前言

提起项目模块化(组件化),相信大家并不陌生。这两年行业内也确实兴起了一股模块化的浪潮,大家都兴致勃勃的投入了其中,感觉要是一个搞研发的没听过模块化,出去跟不好意思跟朋友聊天了。要想实现项目的模块化,就离不开底层路由架构的支持。在这期间,各方大神各显神通,产生了许多优质的开源项目。其中最为出名的自然是阿里出品的 ARouter,大厂出品,必属精品。而公司项目启动模块化大约是在两年前,没能赶上大厂的福利,所以只能自己摸索。当时最大的启发来自于这篇文章:

Android架构思考(模块化、多进程)

文章的作者应该是国内最早一批实践项目模块化的先驱,其提出的:局域网路由解决单进程项目模块化,广域网路由解决多进程项目模块化。至今让人耳目一新,向作者致敬。公司项目模块化的启动就建立在这样的一个基础上,经过两年的业务演变,与当初设计已经大不相同,再加上 kotlin 的引入,是时候整理一下底层的路由框架了。

思考

随着业务需求的发展,项目代码量越来越大,包含的业务模块也越来越多,模块之间耦合的程度也越来越高。随之而来的几个问题,值得我们思考:

  • 项目编译速度越来越慢
  • 业务模块如何解耦和重用
  • 多个小团队之间如何并行开发和测试

这些问题的解决方案其实有两个:插件化模块化

插件化虽然功能强大,但是问题也多,当初考虑到公司项目的实际情况,最终选择了利用路由机制来实现模块化解耦,采用的是前文提到的作者设计的路由框架:ModularizationArchitecture。在这两年的实践过程中,慢慢的遇到了一些问题:

  • 不支持单独简洁的路由跳转
  • 新增一个路由服务的步骤过于繁琐
  • 请求一个路由服务的步骤过于繁琐
  • 提出的 Provider 和 Action 思想维护成本持续增加
  • 设计的路由结果异步回调的方式不是很友好

本想带着这些问题和作者反馈交流,可惜发现作者已经停止维护这个项目了。于是只好自己动手,基于 kotlin 这门语言重新整理设计项目所需要的路由框架。

分析

由于公司项目过于庞大,不利于讲解。为了方便大家理解,所以我们抽取出了一个简洁的工程作为示例:

工程结构如上图所示,并且 module 可以根据项目的业务情况不停的增加,这里只列举了 module-a , module-b 两个 module 作为示例。现在梳理一下,我们的需求是什么:

  • 顶层的 app 可以自由组装不同的 module 实现差异化编译。
  • 不同的业务模块之间没有相互依赖,都是通过依赖底层的 router 来实现路由通信

这样做的带来的好处,刚好可以解决前面我们思考的一些问题:

  • 可以只选择组装自己负责的 module 提升了编译速度
  • 可以快速的测试和发布子应用
  • 多个团队之间可以并行开发各自的 module

而想要实现这样的架构,我们需要解决两个问题:路由框架模块拆分

模块拆分这个问题,我们会在后续的文章中进行整理,这里就不再讨论了,今天主角是路由框架。前面提到过,公司项目其实在两年前就已经引入了一个路由框架,但是在慢慢的实践过程中遇到了许多问题。而我们现在想要做的就是重新整理和设计来解决这些问题。继续梳理一下,我们理想中的路由框架应该是这样的:

  • 支持自由组装不同的 module 实现差异化编译
  • 提供页面路由和拦截器
  • 提供方法路由包括:同步和异步
  • 简洁方便

自由组装 module 进行编译可以减少我们编译代码的体量,提升编译速度。页面路由用来打开其他 module 的界面,拦截器用于处理特殊业务在界面跳转之间的切入,比较典型的就是登录状态的检测。方法路由用来访问其他 module 的功能,提供了同步和异步两种方式。最后的简洁方便,是希望通过编译时注解的方式来解放生产力。那么我们现在就来具体看看今天的主角: XRouter

配置

1.在 root's build.gradle 中加入 jcenter 仓库

allprojects {
    repositories {
        ...
        jcenter()
    }
}

2.在 app's build.gradle 和 module's build.gradle 中启用 kapt

...
apply plugin: 'kotlin-kapt'

3.在 app's build.gradle 和 module's build.gradle 中添加依赖(请使用最新版本)

dependencies {
        ...
    implementation 'com.xuyefeng:xrouter-core:1.1.3'
    kapt 'com.xuyefeng:xrouter-compiler:1.0.6'
}

4.在 app's build.gradle 和 module's build.gradle 中注册路由 Module

kapt {
    arguments {
        arg("XRouterModule", project.getName())
    }
}

5.在 app's build.gradle 中注册路由 App

kapt {
    arguments {
        arg("XRouterApp", project.getName() + ",modulea,moduleb")
    }
}
  • 路由 App 由三个路由 module 构成,分别是 app、modulea 和 moduleb。对应的是第4步注册的路由 module 的工程名称
  • 这里可以根据自由组装的 module 动态设置 XRouterApp

6.初始化 XRouter

@RouterApp
class BaseApplication : Application() {

    override fun onCreate() {
        super.onCreate()
        XRouter.init(this, BuildConfig.DEBUG)
    }
}
  • 推荐在 Application 中初始化,并且添加注解 @RouterApp
  • 在 debug 模式下,可以使用 XRouter 作为 tag 过滤日志信息

使用

页面路由

1.注解页面(支持一个页面对应多个路由地址)

@Router("www.baidu.com")
class MainActivity : AppCompatActivity()

@Router("www.baidu.com", "www.google.com")
class MainActivity : AppCompatActivity()

2.跳转页面

// 常用跳转
XRouter.with(context).target("www.google.com").jump()

// 自定义intentFlags
XRouter.with(context)
        .target("www.google.com")
        .intentFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
        .jump()

// 自定义跳转动画
XRouter.with(context)
        .target("www.google.com")
        .transition(android.R.anim.fade_in, android.R.anim.fade_out)
        .jump()

// 通过url拼接参数
XRouter.with(context)
        .target("www.google.com?name=blue&age=18")
        .jump()

// 通过bundle传递参数
XRouter.with(context)
        .target("www.google.com")
        .data("name", "blue")
        .data("age", 18)
        .data(Bundle())
        .jump()

// startActivityForResult
XRouter.with(context)
        .target("www.google.com")
        .requestCode(1001)
        .jump()

// 路由结果,只关心成功
XRouter.with(context)
        .target("www.google.com")
        .jump {
            // jump success to do sth
        }

// 路由结果,只关心失败
XRouter.with(context)
        .target("www.google.com")
        .jump({
            // jump failure to do sth
        })

// 路由结果,包含失败和成功
XRouter.with(context)
        .target("www.google.com")
        .jump({
            // jump failure to do sth
        }, {
            // jump success to do sth
        })

3.拦截器

@RouterInterceptor(priority = 8)
public class LoginInterceptor implements XRouterInterceptor {
    @Override
    public void onInit(@NotNull Context context) {
        // do something in application init
    }
    @Override
    public void onProcess(@NotNull XRouterInterceptorCallback callback) {
        // check login status
        ...
        // check success
        callback.onContinue();
        // or check failure
        callback.onIntercept("check login error");
    }
}
  • 注解参数 priority 决定拦截器的优先级,默认是5,数值越大,优先级越高
  • 可以定义多个拦截器,根据优先级依次执行
  • onInit 方法在 XRouter 初始化的时候被调用,可以用于做拦截器初始化
  • onProcess 方法在页面路由中被调用,可以用于做页面拦截。经典场景是页面路由的过程中需要检测登录状态,如果登录状态失效,终止原路由,改为跳转至登录界面

方法路由

1.注解方法

@Router("toast")
fun toast(context: Context, routerParams: XRouterParams): XRouterResult {
    Toast.makeText(context, "toast from other module", Toast.LENGTH_SHORT).show()
    return XRouterResult.Builder().build()
}

@Router("getSum", async = true)
fun getSum(context: Context, routerParams: XRouterParams, callback: XRouterCallback?) {
    // 获取对象数据
    val fragment = routerParams.obj as Fragment
    // 获取普通数据
    val a = routerParams.data.getInt("a")
    val b = routerParams.data.getInt("b")
    ...
    val sum = a + b
    val result = XRouterResult.Builder().data("sum", sum).obj(fragment).build()
    callback?.onRouterSuccess(result)
    // or
    callback?.onRouterError(result)
}
  • 注解的方法需要为全局的静态方法
  • 注解的名称可以跟方法名不同,注解的名称用于查找目标方法,方法名用于执行该方法
  • 默认为同步路由,可以通过注解参数 async 开启异步路由
  • 所有的同步路由接收的参数都是 Context 和 XRouterParams,请保持写法一致
  • 所有的异步路由接收的参数都是 Context、XRouterParams 和 XRouterCallback,请保持写法一致
  • 路由传递过来的参数可以通过 XRouterParams 获取,路由结果的参数可以通过 XRouterResult 设置,都采用 data 传递普通参数(用法参照 bundle),obj 传递对象数据

2.调用方法

// 无参路由
XRouter.with(context).target("toast").route()

// 通过bundle传递参数
XRouter.with(context)
        .target("doSomething")
        .data("name", "blue")
        .data("age", 18)
        .route()

// 对象传递
XRouter.with(context)
        .target("doSomething")
        .obj(Fragment())
        .route()

// 同步路由获取结果
val result = XRouter.with(context)
        .target("getSum")
        .data("a", 1)
        .data("b", 2)
        .route()

// 异步路由获取结果,只关心成功
XRouter.with(context)
        .target("getSum")
        .data("a", 1)
        .data("b", 2)
        .route {
            // get params
            val sum = it.getData().getInt("sum")
            val fragment = it.getObj() as Fragment
            // route success to do sth
        }

// 异步路由获取结果,只关心失败
XRouter.with(context)
        .target("getSum")
        .data("a", 1)
        .data("b", 2)
        .route ({
            // route failure to do sth
        })

// 异步路由获取结果,包含失败和成功
XRouter.with(context)
        .target("getSum")
        .data("a", 1)
        .data("b", 2)
        .route ({
            // route failure to do sth
        },{
            // get params
            val sum = it.getData().getInt("sum")
            val fragment = it.getObj() as Fragment
            // route success to do sth
        })

混淆代码

-keep class com.blue.xrouter.** {*;}
-keep interface com.blue.xrouter.** {*;}

总结

关于路由框架,各家各户的实现都大同小异,思路基本上都是相通的,只是设计上的差异罢了。其实路由框架本身并没有太多的技术难点,因此本文并不是讨论什么高深的技术,只是针对实际项目路由框架的演变做了一些整理和思考。并且随着项目的继续发展,这也将是一个持续化的过程。以上就是公司项目在模块化道路上的一些探索,由于本人水平有限,难免有不足之处,望各位不吝指出,不胜感激。

最后再附上:github地址传送门 喜欢就star一下呗

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