组件化方案:JIMU之UI路由(一)
背景介绍:
张明庆老哥之前在得到工作时,开源了DDAndroidComponent项目,演示组件化思路及实现,本人当时作为协作者参与了一部分开发,现项目迁移到JIMU ,接下来将在新项目仓库进行维护。
张明庆老哥的几篇文章:
1、Android彻底组件化方案实践
2、Android彻底组件化demo发布
3、Android彻底组件化-代码和资源隔离
4、Android彻底组件化—UI跳转升级改造
5、Android彻底组件化—如何使用Arouter
为什么要有这一篇
首先,不在此处展开组件化核心思想“隔离与发现”,因为有了隔离的要求(其他模块的Activity在编译期不可见,无法创建显示Intent),和JIMU本身自有一套路由(我们称之为“路由”,“IOC容器”,“DI组件”都是可以的,本质就是用来注册和寻找实例的)的实际基础存在,这决定了在组件化中跨模块跳转页面,需要使用以下的一种技术:
- 将模块内的页面跳转,整理出API,在ComponentService中暴露。
- 抽象页面跳转的过程,使用映射关系,由路由短链发起跳转请求。
其次,为什么Demo中没有直接使用ARouter等成型方案?ARouter是个很优秀的项目,但是对于JIMU而言,他有点over-weight而且功能重复,我前面简单提到:“JIMU本身自有一套路由”,而且在JIMU中自动注册的功能是注册到该路由的,再加入ARouter纯粹多余(选择多了往往是麻烦事),以及在项目中使用ARouter的,可以参考链接中的第五篇,使用ARouter进行路由跳转,甚至是基于ARouter实现组件化,以达到项目的简洁。
结合讨论群中一些朋友们提出的问题,以及github上的典型issue,本文进行一些扼要的总结,便于大家排错。
为了方便,我们下文将用UIRouter来代指JIMU中提供的UI路由。
以下是outline:
- UIRouter 1.0.0提供哪些东西
- UIRouter 1.0.0包含哪些已知问题
- 如何集成 UIRouter
- UIRouter的特性概述
- 常见问题Q/A
UIRouter 1.0.0提供哪些东西
- 依赖库:router-annotation 包含了可用的注解以及内部使用的实体类、帮助类
- 注解处理器 router-anno-compiler
可供使用的注解:(代码取自新版本,和v1.0.0存在一定小差异)
- RouteNode 路由节点,对应Activity
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface RouteNode {
/**
* path of one route
*/
String path();
/**
* The priority of route.
*
* we inspect the path and throw exception when duplicated
* paths were find, thus, it's useless and impossible to use priority
*/
@Deprecated
int priority() default -1;
/**
* description of the activity, user for gen route table
*/
String desc() default "";
}
- Autowired 用于从intent中获取参数,并将值注入Field的注解
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.CLASS)
public @interface Autowired {
/**
* @return param's name or service name.
*/
String name() default "";
/**
* <em>primitive java type check will be ignore</em>
* check the result of DI, if inject failed, the value of
* the field will be null, if required, output log
*
* @return true for required,false otherwise
*/
boolean required() default false;
/**
* throw exception when the required field is null after inject.
* <p>
* It can help developer find most data delivering bugs when developing.
* but not suggest to open this function after release.
* <p>
* I suggest to define a Constant maintained manually
* <p>
* only activated when required = true and throwOnNull = true.
*
* @return true if throwing exception when null is required, false otherwise
*/
boolean throwOnNull() default false;
/**
* @return field description
*/
String desc() default "none desc.";
}
其他非暴露供使用的内容不做介绍。
UIRouter 1.0.0包含哪些已知问题
- 因单例模板出现问题导致:本设计成单例的JsonService和AutowireService不是单例
- 在一个Module中进行java和kotlin的混编并同时需要使用RouteNode注解时存在问题
如何集成 UIRouter
首先:您应该以及集成了JIMU方案,使用了gradle plugin 并且集成了基础库:
以下演示代码均建立在java项目、gradle plugin版本<3.0 基础上
- 集成注解依赖库:
compile 'com.luojilab.ddcomponent:router-annotation:1.0.0'
注意:componentLib包中已经包含了注解依赖库,所以不需要再声明依赖库。
- 集成注解处理器:
annotationProcessor 'com.luojilab.ddcomponent:router-anno-compiler:1.0.0'
注意:在Module的build.gradle中声明,不要在底层库中声明;不声明无法使用UIRouter功能。
并指定Module的环境参数:
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [host: "share"]
}
}
}
注意:这里的环境参数会影响使用时编写的url(or URI),以此处代码为例,我们的url形式是这样的:
[schema]//[host][path]?[queryString]
举个例子,该Module中存在一个RouteNode:path为“/index”
那么对应的url为:
【任意协议】//share/index
如果未指定,那么将使用默认值“default”,即:
【任意协议】//default/index
3.将生成的映射注册到UIRouter
抱歉在之前的内容中,遗漏了这一块,给使用者带来了困扰
这一部分内容,在阅读上而言放在后面会更好一点,但是容易被忽视。
在使用中,我们会得到自动生成的路由映射,一定要注册到UIRouter;例如:
public class ShareApplike implements IApplicationLike {
UIRouter uiRouter = UIRouter.getInstance();
@Override
public void onCreate() {
uiRouter.registerUI("share");
}
@Override
public void onStop() {
uiRouter.unregisterUI("share");
}
}
我们选择的是在组件的生命期入口进行注册和反注册。不多做赘述。
实际上这样我们就完成了集成,接下来就是使用了。
为路由节点(Activity)添加注解
注意,priority没有实质性意义,已废弃,Module中不允许出现同样的path
@RouteNode(path = "/main", desc = "首页")
public class MainActivity extends BaseActivity implements View.OnClickListener {
//...代码略去
}
以appModule中的MainActivity为例添加了一个节点。这样我们可以得到一个生成类:AppUiRouter:
public class AppUiRouter extends BaseCompRouter {
@Override
public String getHost() {
return "app";
}
@Override
public void initMap() {
super.initMap();
routeMapper.put("/main",MainActivity.class);
}
}
一个用于辅助的Module路由表清单txt:AppRouterTable.txt
auto generated, do not change !!!!
HOST : app
首页
/main
进行跳转
UIRouter.getInstance().openUri({context},
"JIMU://app/main", {bundle});
关于参数
将在(二)中详细展开,这里简单介绍一下取参数,取参数使用了Autowired注解,进行DI。
以一个新版中的演示Demo为例:
@RouteNode(path = "/uirouter/demo/2", desc = "使用bundle传递参数")
public class Demo2Activity extends TestActivity {
private static Bundle bundle = new Bundle();
static {
bundle.putString("foo", "foo string");
bundle.putString("EXTRA_STR_BAR", "bar string");
}
@Autowired() //不指定名称时将使用变量名,若被混淆可能出现问题,
// 建议使用name指定key,参考bar的使用
String foo;
@Autowired(name = "EXTRA_STR_BAR")
String bar;
public static final UiRouterDemoActivity.Case aCase
= new UiRouterDemoActivity.Case(false,
"使用bundle传递参数",
"JIMU://app/uirouter/demo/2",
bundle);
@Override
protected void displayInfo(TextView textView) {
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("使用bundle传递参数成功\r\n");
stringBuilder.append("foo:").append(foo).append("\r\n");
stringBuilder.append("bar:").append(bar).append("\r\n");
textView.setText(stringBuilder.toString());
}
}
父类的代码:
abstract class TestActivity extends AppCompatActivity {
private TextView textView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test);
textView = findViewById(R.id.demo_tv_info);
AutowiredService.Factory.getSingletonImpl()
.autowire(this);
displayInfo(textView);
}
protected abstract void displayInfo(TextView textView);
}
稍微细说一下:
需要被注入的Field为:
@Autowired() //不指定名称时将使用变量名,若被混淆可能出现问题,
// 建议使用name指定key,参考bar的使用
String foo;
@Autowired(name = "EXTRA_STR_BAR")
String bar;
建议:指定name!
我们看看生成的内容中,多了哪些:
- 路由 AppUiRouter.java 中:
public class AppUiRouter extends BaseCompRouter {
@Override
public String getHost() {
return "app";
}
@Override
public void initMap() {
super.initMap();
routeMapper.put("/main",MainActivity.class);
routeMapper.put("/uirouter/demo/2",Demo2Activity.class);
paramsMapper.put(Demo2Activity.class,new java.util.HashMap<String, Integer>(){{put("foo", 8); put("EXTRA_STR_BAR", 8); }});
}
}
相比于MainActivity,多了一些东西:paramMapper中添加了一些配置,这里不详细展开,在不hook系统API的情况下,都会回归到使用Intent启动Activity。而传参依旧需要使用Bundle,我们知道bundle的读写是需要知道类型的。UIRouter支持的类型下一篇展开
- Demo2Activity$$Router$$Autowired.java文件
以下代码仅演示下,不做展开,读者不用深究,将在(二)中详细展开
类似:
/**
* Auto generated by AutowiredProcessor */
public class Demo2Activity$$Router$$Autowired implements ISyringe {
private JsonService jsonService;
@Override
public void inject(Object target) {
jsonService = JsonService.Factory.getSingletonImpl();
Demo2Activity substitute = (Demo2Activity)target;
substitute.foo = substitute.getIntent().getStringExtra("foo");
substitute.bar = substitute.getIntent().getStringExtra("EXTRA_STR_BAR");
}
@Override
public void preCondition(Bundle bundle) throws ParamException {
}
}
- 路由表中多了以下内容:
使用bundle传递参数
/uirouter/demo/2
foo:String
EXTRA_STR_BAR:String
注意:多了参数信息,是 name的值 和 参数的类型
发起注入的核心代码:和v1.0.0的API有一定区别
AutowiredService.Factory.getSingletonImpl().autowire(this);
UIRouter的特性概述
时间和篇幅原因,移到(二)中展开。
常见问题Q/A
- Q:为什么无法跳转?
A:根本原因都是没有正确集成:排查次序
- 是否集成了注解库和注解处理库?
- 同Module中path是否有重复?
- gradle任务message中是否有异常输出?
- 路由映射是否注册到UIRouter
- 是否url有误?
- 是否有参与检测的参数,但是没有包含或者有误?(对于这一点还不清楚的,请等待第二篇文章)
- 生成的UIRouter是否在APPLike的onCreate生命周期节点中注册到UIRouter
- 组件会维护两份manifest文件,是否遗漏添加(注:异常已被Router捕获并处理为:将目标加入黑名单,故没有直观的crash)
- Q:出现了ClassNotFoundException怎么办?
A:应该是启用了混淆,添加免混淆配置:可能因为项目变动的原因,后期会修改生成类path,一切以项目主页为准
-keep class com.luojilab.router.** {*;}
-keep class com.luojilab.gen.** {*;}
-keep class * implements com.luojilab.component.componentlib.router.ISyringe {*;}
3.Q:出现了错误很难排查怎么办?
A:根本原因是我当时和张老哥没有协调好,导致1.0.0的代码过早发出,而迭代版本因为其他原因迟迟未发,四月份一定发版本,新版本中log的输入以及防御性代码比较完善,应该可以提供充足的纠错信息。
4.Q:gradle plugin >=3.0 集成问题?
A:首先注意,我最开始就将依赖库和注解处理库分开了,这样已经避免了重复使用api和annotationProcessor声明同一个库的各种问题,如果在底层库中集中添加注解依赖库,使用api(或者还未移除的compile),不要使用Implementation;若是在组件Module中,随意使用api或Implementation。但是必须使用annotationProcessor声明注解处理库(使用compile已经不会自动添加到注解处理包路径下)
- Q:kotlin是否可以用?
A:可以使用,如何集成参考demo,注意,对java和kt的Activity都使用注解,仅需要使用kapt声明注解处理器即可,按照Demo集成kapt3即可,不需要声明annotationProcessor,禁止使用早已废弃的apt插件
(Q/A持续更新)
下一篇将详细展开UIRouter的基础功能特性、新版本特性,并会安排发一个迭代版本。
JIMU的讨论群,群号693097923,欢迎大家加入:
imageqq群中有很多热心的朋友,一些重要的讨论,往往从群里面展开,最后转移到项目的issue中展开讨论以及总结。