企业🤖AI Agent构建引擎,智能编排和调试,一键部署,支持私有化部署方案 广告
# 健康猫android路由组件解析 2017年中我们项目已经开始全面实行模块化,新增模块一律使用新module来进行开发,这样一来可以通过分解业务来降低应用的复杂性,独立模块还可以独立运行,减少构建规模,提高开发速度。 实际上我们的模块化执行也比较规范,但依然存在一些和模块化相悖的细节问题,譬如在一个独立的业务模块里面引用了其他**业务模块**。后来了解到这样做主要是因为需要调用其他模块已经存在的接口方法,假如再写一个,会造成重复实现,也给维护增加了难度。这些都是因为我没有跟大家说明清楚所导致的,所以我把一些重要的方法和概念写到这份文档里,同时带来一些新的功能来简化大家使用路由组件。 ## 1. 什么是路由 这里路由特指: 1. 将一串字符串/地址映射到某一页面; 2. 解析这串字符串/地址,可以得到映射的页面; 3. 通过这串字符串/地址作为目标进行跳转,可以进入映射的页面; 这一系列的解决方案,我们称之为路由。后来因为路由同样解决了模块间的跳转问题,所以把模块间的方法调用也归为路由组件要解决的问题。 ## 2. 为什么需要路由 我们的项目加入路由是因为一个后台动态配置首页菜单的需求。当时我们已经在酝酿模块化的相关,所以当这个需求出现的时候,就毫不犹豫的选择了这个虚拟地址映射的解决方案。 从技术上来说,就是Android中要打开一个新的页面,需要一个有目标页面的`Class`或者完整的包名的意图`Intent`。我们采用模块化开发的过程中,当我们要跳转到另一个业务模块的页面时候,我们并没有对应页面的`Class`,那么开发工作就基本无法进行下去了。如果采用完整包名或者反射的方式获取`Class`,虽然开发可以继续,编译也能通过,但是运行到这里就会停止。即使你再加上错误处理,这个虽然解决了暂时性的问题,但是一个完整的包名抽象的程度太低,无法应用到其他地方,譬如我们无法把这串包名给iOS的应用让他们跳转到这个页面。 所以,在这样一个需要多端统一跳转和模块化的需求下,采用这个已经成熟的方案实际上是水到渠成。于是就有了我们最初的路由组件`Router`,下面我给大家介绍这里面的主要功能和使用方法。 ## 3. 路由组件`Router`的功能 路由主要是两个功能,一个是解析一串地址进行页面跳转,这次会带来一个新的功能来进行模块间的方法调用。 其实我们的路由库里面还有一个第三方的消息总线库EventBus,这个我就不多说了。 * **页面跳转** 上面已经介绍过,通过解析一串字符串地址来得到映射的页面和参数,从而进行页面跳转或者错误处理。也因为使用了字符串,所以可以使用路由的场景不再局限于java代码中,例如: 1. 内置浏览器跳转原生页面 通过重写`shouldOverrideUrl`方法,里面判断要跳转的地址是否已经映射到原生页面,从而进行路由选择。这样处理可以让我们在浏览H5页面的时候无缝切换到原生页面,开发上对混合开发的效率也提高不少。 2. 外部浏览器唤起应用并跳转到对应页面 通过配置好manifest的相关信息,可以在外部浏览器地址输入应用地址唤起App,唤起页再通过路由来解析地址来进入指定页面。这种方式给外部导流带来了便捷。 3. 应用内跳转 在应用内,任何需要跳转的地方都可以使用,无论是消息的页面跳转,还是普通的跳转,后台配置的动态菜单入口,轮播图跳转,只要是正确的地址,不管自身模块是否有对应的页面映射,最终集成的应用都可以正确的处理,并且有足够的灵活性。 * **方法调用** 模块化开发中,有一类模块间通信的需求,就是跨模块调用api,譬如需要调用其他模块的特定接口。如果自己在本模块内重新实现一遍,以后接口改动的时候维护起来比较麻烦,所以目前大家采取了直接引用其他业务模块的方式。这跟模块化要解决的问题相悖,因为这相当于重新引入了复杂度和规模。当这些需求逐渐增多的时候,你会发现,你的“独立模块”实际上就是一整个应用。 为了解决这个问题,健康猫的公共模块其实有一个解决的类,叫`HMActionHelper`。里面有一系列模块需要暴露出来的公共接口,并且在主项目初始化的时候注入各模块的实现。虽然这种方案也是面向接口,但是实现的方式不太优雅,后期的扩展能力也比较弱,而且跟我们的路由组件库完全独立,所以新版本(1.0.35以后)在`Router`中增加了此功能,下面会给大家介绍使用方法。 ## 4. 使用方法 页面跳转现在可以通过注解来进行快速生成映射和页面参数映射,后续计划会把api调用也用注解进行简化。 ### **页面跳转** * (旧)- 首先要建立 地址-页面 的映射,由于每个app的host都是一样的,所以映射的地址部分只是path部分,例如: Router.addActivity("/Setting/BlackList", BlacklistManagementActivity.class); 在需要跳转的地方,使用Router来解析整个地址串: Router.prepare(context, "healthmall://healthmall.cn/Setting/SettingBlack").go(); 如果需要页面参数,就在地址中按照[我们项目的规则](http://10.0.2.21:8090/pages/viewpage.action?pageId=26869801)添加参数,并且在打开页面的intent中获取对应的值: // 地址 String url = "healthmall://healthmall.cn/Setting/SettingBlack?params={\"id\"=1}"; // 页面中 int id = getIntent().getIntExtra("id"); // 但是在健康猫项目中,由于刚开始时候没有处理类型的问题,所以如果是取int值,实际上是下面这样的: int id = (int) Double.pareDouble(getIntent().getStringExtra("id")); // 在太阳神项目中,这个问题被修复了,但是健康猫的为了保持现有的兼容,暂时不去改变。 // 但我们后面介绍注解的方式,则为这两种情况都做了兼容,所以注解的使用方式不需要区分项目。 * **(新)**- 新的使用方式是通过注解来轻松解决以上重复代码。 首先需要导包: implementation "com.healthmall.android:router-annotation:$router_version" // 注解包 // 如果不需要kotlin支持 annotationProcessor "com.healthmall.android:router-compiler:$router_version" // 注解处理 // 如果需要kotlin kapt "com.healthmall.android:router-compiler:$router_version" 模块的gradle配置: defaultConfig{ ... javaCompileOptions { annotationProcessorOptions { arguments = [moduleName: project.getName()] } } } 地址映射部分,使用`RouterPath`注解来标明这是一个路由页面,注解的value为地址的path部分,例如: @RouterPath("/Setting/SettingBlack") public class BlacklistManagementActivity extends ... { ... } 注解处理器会生成跟上面(旧)一样的映射代码,存放在对应一个叫(模块名+RouterInit)的类的`init`静态方法里面,例如`module_lifeclub`,会生成一个叫`LifeclubRouterInit`的类,然后在主工程里面调用`init`方法为生活馆模块的路由页面统一建立映射即可。 页面参数部分,使用`@RouterParam`注解来注入到一个类变量,注解的value为参数的key,然后在使用这些成员之前,先调用`Router.inject(activity)`来完成参数注入。例如: @RouterPath("/Setting/SettingBlack") public class BlacklistManagementActivity extends ... { // 注意属性要是public类型 @RouterParam("id") public int id; @Override public void onCreate(Bundle saveInstance) { Router.inject(this); ...id... } } 注解处理器会为拥有`@RouterParam`的路由页面生成一个`ActivityName_Inject`的类,在开发中我们无须关注,有兴趣的可以看一看生成的代码。 要注意的是在**Kotlin Class**中,因为kotlin使用标准化get set,所以按照Java的方式去写成员变量,生成的字节码里面成员变量是私有的,而我们的Inject类是java代码,没办法直接访问到,所以需要`@JvmField`注解来帮忙生成公开的变量。目前没找到好的办法区分源文件是哪种格式,所以暂时先这样吧: @RouterPath("/Setting/SettingBlack") class BlacklistManagementActivity: Activity { @RouterParam("id") @JvmField var id: Int = 0 @RouterParam("name") lateinit var name: String } 如果参数的类型是`String`,可以选择使用`lateinit`关键字来代替使用`@JvmField`,但是如果你的参数不一定会被初始化的话,不建议使用。因为`lateinit`关键字修饰的是非空类型,在给它设置值之前都是不能使用的,空操作符对它也不会生效(因为初始化值之前调用都会报错)。如果路由定义为可空但你非选择这样使用,那么可以在调用它之前,用`::属性名.isLateinit`来反射得到它是否已经初始化。总之是十分不推荐。 ### 跨模块方法调用 为什么会有跨模块方法调用这种需求,上面已经阐述明确,现在直接看一下使用方法。 首先在公共模块下定义好接口并继承`com.gzdxjk.ihealth.router.action.Action`,然后使用`Router.find`来找到对应的实现类或者代理。实现者需要实现定义的接口,并且在主项目运行时尽可能早的注入实现类。如果没有实现类,接口方法调用将不会发生,并且返回值为`null`。 示例: package common; // 公共模块中定义接口 public interface TestAction extends Action { void test(String id, ApiCallBack<String> callback); } ---- package other; // 其他模块实现接口 class TestActionImpl1 implements TestAction { @Override public void test(String id, ApiCallBack<String> callback) { ... } } ---- package caller; ... // 目标模块方法调用 Router.find(TestAction.class).test("9527", new ApiCallBack<String>(){ ... }); ---- // 主模块注入实现,后续会改成注解,无须再手动注入 Router.add(new TestActionImpl1());