美文网首页Android收藏集android应用框架
组件化方案:JIMU之UI路由(一)

组件化方案:JIMU之UI路由(一)

作者: leobert | 来源:发表于2018-04-07 23:50 被阅读511次

    背景介绍:

    张明庆老哥之前在得到工作时,开源了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 基础上

    1. 集成注解依赖库:
    compile 'com.luojilab.ddcomponent:router-annotation:1.0.0'
    

    注意:componentLib包中已经包含了注解依赖库,所以不需要再声明依赖库。

    1. 集成注解处理器:
    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

    1. Q:为什么无法跳转?
      A:根本原因都是没有正确集成:排查次序
    • 是否集成了注解库和注解处理库?
    • 同Module中path是否有重复?
    • gradle任务message中是否有异常输出?
    • 路由映射是否注册到UIRouter
    • 是否url有误?
    • 是否有参与检测的参数,但是没有包含或者有误?(对于这一点还不清楚的,请等待第二篇文章)
    • 生成的UIRouter是否在APPLike的onCreate生命周期节点中注册到UIRouter
    • 组件会维护两份manifest文件,是否遗漏添加(注:异常已被Router捕获并处理为:将目标加入黑名单,故没有直观的crash
    1. 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已经不会自动添加到注解处理包路径下

    1. Q:kotlin是否可以用?
      A:可以使用,如何集成参考demo,注意,对java和kt的Activity都使用注解,仅需要使用kapt声明注解处理器即可,按照Demo集成kapt3即可,不需要声明annotationProcessor,禁止使用早已废弃的apt插件

    (Q/A持续更新)


    下一篇将详细展开UIRouter的基础功能特性、新版本特性,并会安排发一个迭代版本。

    JIMU的讨论群,群号693097923,欢迎大家加入:

    image

    qq群中有很多热心的朋友,一些重要的讨论,往往从群里面展开,最后转移到项目的issue中展开讨论以及总结。

    相关文章

      网友评论

      • 等一个晴天_4eeb:引入了butterknife和dagger后不能生成RouterTable,有谁遇到过?
        leobert:@等一个晴天_4eeb dagger没有具体测试,生成代码应该和dagger没有关系
      • developerYk:从 JIMU 项目学习到了很多,感谢作者!
        developerYk:@leobert 真的太感谢了
        leobert:@developerYk 无论是从别的项目出发思考还是自己产出内容,都会有“学习”和“收获”😀,项目使用者支持者提出的建议和建设性问题也让我受益良多。
      • 3db02335ba2e:罗伯特,是在说我吗
        leobert:群里面的朋友都是很热心的😂,你也是其中一位👍

      本文标题:组件化方案:JIMU之UI路由(一)

      本文链接:https://www.haomeiwen.com/subject/jzluhftx.html