带你打造一个多Module路由框架

作者: Ms__Liu | 来源:发表于2017-04-17 00:15 被阅读1043次

    我过我要的生活
    不是生活过我就好
    只要是我想要的
    期待多久都有情调

    开篇推荐一首歌曲,送给一直默默奋斗、坚持,但有时又感到迷茫的你。猛戳《过我的生活》


    项目GitHub地址:https://github.com/ms-liu/AndroidRemoteDemo

    一、项目为什么要分成多Module

    先感受下传统的两种划分模式:业务功能分包、不同组件分包。


    业务功能划分 不同组件划分
      这两种分包都是放在一个App这一个Module里面的。
      每次编译往往都是需要将项目的所有业务都编译一遍,过程繁重。对于单元测试也只是想想就好,因为和普通测试根本没差。另外经常在开发人员之间发生的事情,就是为谁修改了谁的代码而发生互怼事件 [斜眼笑]
      总体体现:
    • 编译困难
    • 单元测试困难
    • 开发、维护困难

    因此将项目分成多个Module是很有必要的,这样可以:

    • 提高灵活性——每次只需要编译我们自己想要的Module即可,同时提升编译速度。
    • 方便单元测试——可以轻松的针对每个Module作出单元测试。
    • 让职责分工明确——可以更好的让开发人员去维护各自的模块
    多Module划分

    二、解决Module之间页面跳转问题

    1、问题分析

    项目被分成多Module之后,由于项目业务往往是交叉的,所以Module当中包含的页面跳转往往也是相互的。
      为了能够让页面Module之间的页面能够相互跳转,我们往往需要将每个Module之间相互compile,这样无异于又增加了模块之间耦合性,并且还有可能会报出一个循环编译的警告(warning recycle compile)

    一般多模块之间关系
    2、问题解决

    通过参考Web页面的跳转方式,不难想到的是我们完全可以仿照Web跳转,也先去为每一个Activity注册一个地址,然后,然后.....(PS:然后就没有思路了,MDZZ)。这时看图一张:

    多Module路由关系

    首先去关心每一个需要被其它Module调用的页面,我们将它配置到Common Module中的Remote Configure文件。
      下面我们就是在Remote模块当中编写一些具体Router Operator类。下面我们就需要思考对一个路由地址究竟需要进行怎样的处理了。

    • 我们需要拿到这个地址(注册)——通过put/add方法
    • 我们需要让路由地址生效(调用)——通过invoke方法
    3、具体代码编写
    (一)定义操作接口

    核心方法就是put和invoke

    /**
     * ==============================================
     * <p>
     * 类名:IOperator
     * &nbsp 基本操作接口
     * <p>
     * 作者:M-Liu
     * <p>
     * 时间:2017/3/27
     * <p>
     * 邮箱:ms_liu163@163.com
     * <p>
     * ==============================================
     */
    
    public interface IOperator<T,K> {
        /**
         * 添加路由地址
         * @param uri 路由地址
         * @param clazz 路由类型
         */
        void put(String uri,Class<T> clazz);
    
        /**
         * 执行路由路线
         * @param context Context
         * @param uri 路由地址
         * @return {@link BaseIntentOperator#invoke(Context, String)}
         */
        K invoke(Context context,String uri);
    
        /**
         * 检查当前路由路线 是否存在
         * @param uri 路由地址
         * @return
         */
        boolean check(String uri);
    }
    
    (二)基础实现类

    因为是Demo讲解,所以只是针对Intent这一种

    /**
     * ==============================================
     * <p>
     * 类名:BaseIntentOperator
     * &nbsp 返回类型是Intent的基础操作类
     * <p>
     * 作者:M-Liu
     * <p>
     * 时间:2017/3/27
     * <p>
     * 邮箱:ms_liu163@163.com
     * <p>
     * ==============================================
     */
    
    public abstract class BaseIntentOperator<T> implements IOperator<T,Intent> {
        private HashMap<String,Class<T>> mIntentContainer;
        public BaseIntentOperator(){
            mIntentContainer = new LinkedHashMap<>();
        }
    
    
        /**
         * {@inheritDoc}
         */
        @Override
        public void put(String uri, Class<T> clazz) {
            if (mIntentContainer != null){
                mIntentContainer.put(uri,clazz);
            }
        }
    
        /**
         * {@inheritDoc}
         */
        @Override
        public Intent invoke(Context context, String uri) {
            Class<T> clazz = null;
            if (check(uri)){
                clazz = mIntentContainer.get(uri);
            }
            if (clazz == null){
                throwException(uri);
            }
            return new Intent(context,clazz);
        }
    
        public abstract void throwException(String uri);
    
        /**
         * {@inheritDoc}
         */
        @Override
        public boolean check(String uri) {
            return mIntentContainer != null && mIntentContainer.keySet().contains(uri);
        }
    }
    
    (三)具体Activity这种处理

    当中定义了PROTOCOL 常量,相当于Http这种协议,在Demo中是预留,因为没有处理Service等其它组件的情况,所以没有用到。

    /**
     * ==============================================
     * <p>
     * 类名:ActivityIntentOperator
     * &nbsp 针对Activity路由操作
     * <p>
     * 作者:M-Liu
     * <p>
     * 时间:2017/3/28
     * <p>
     * 邮箱:ms_liu163@163.com
     * <p>
     * ==============================================
     */
    
    public class ActivityIntentOperator extends BaseIntentOperator<AppCompatActivity> {
        public static final String PROTOCOL = "activity://";
    
        @Override
        public void throwException(String uri) {
            throw new NotFoundRuleException(ActivityIntentOperator.class.getCanonicalName(),uri);
        }
    }
    
    (四)Manager管理者编写
    public class RemoteOperatorManager {
    
       private static RemoteOperatorManager mRemoteManager;
       //路由操作管理池
       private HashMap<String,IOperator> mOperatorPool;
       private RemoteOperatorManager(){
           mOperatorPool = new LinkedHashMap<>();
           putDefaultOperator();
       }
    
       //初始化默认路由操作
       private void putDefaultOperator() {
           if (mOperatorPool != null){
               mOperatorPool.put(ActivityIntentOperator.PROTOCOL,new ActivityIntentOperator());
           }
       }
    
       /**
        * 获取RemoteOperatorManager
        * @return RemoteOperatorManager
        */
       public static RemoteOperatorManager get(){
           if (mRemoteManager == null){
               synchronized (RemoteOperatorManager.class){
                   mRemoteManager = new RemoteOperatorManager();
               }
           }
           return mRemoteManager;
       }
    
       /**
        * 添加自定义 路由操作
        * @param protocol 路由协议 {@link ActivityIntentOperator#PROTOCOL}
        * @param operator 具体操作类
        */
       public RemoteOperatorManager putCustomOperator(String protocol,IOperator operator){
           if (mOperatorPool != null){
               mOperatorPool.put(protocol,operator);
           }
           return mRemoteManager;
       }
    
    
       /**
        * 检查当前路由操作 是否存在
        * @param  uri 路由地址
        * @return false 不存在 true 存在
        */
       public boolean checkOperatorForURI(String uri){
           if (!TextUtils.isEmpty(uri)){
               IOperator<?, ?> operator = getOperator(uri);
               if (operator == null){
                   throw new NotFoundRuleException(uri);
               }
               return true;
           }else {
               throw new NotFountRemotePathException();
           }
       }
    
       public boolean checkOpratorForProtocol(String protocol){
           return mOperatorPool != null && mOperatorPool.keySet().contains(protocol);
       }
    
       /**
        * 根据Uri获取路由操作类
        * @param uri 路由地址
        */
       public  <T,V> IOperator<T,V> getOperator(String uri){
           IOperator<T,V> operator = null;
           if (mOperatorPool != null){
               Set<String> protocols = mOperatorPool.keySet();
               for (String protocol :
                       protocols) {
                   if (uri.startsWith(protocol)){
                       operator = mOperatorPool.get(protocol);
                       break;
                   }
               }
           }
           return operator;
       }
    
       public <T> RemoteOperatorManager putRemoteUri(String uri, Class<T> clazz) {
           if (checkOperatorForURI(uri)){
               IOperator<T, ?> operator = getOperator(uri);
               operator.put(uri,clazz);
           }
           return mRemoteManager;
       }
    }
    
    (五)代言者——Remote编写

    为什么是代言者,是因为其实每一个具体方法都是由Manager去完成的

    public class Remote {
    
        private final static String PATTERN = "";
    
        /**
         * 添加自定义路由操作
         * @param protocol 路由协议
         * @param operator 路由操作类
         * @return
         */
        public static RemoteOperatorManager putCoustomOprator(String protocol, IOperator operator){
            return RemoteOperatorManager.get().putCustomOperator(protocol,operator);
        }
    
        /**
         * 添加路由地址
         * @param uri 路由地址
         * @return
         */
        public static<T> RemoteOperatorManager putRemoteUri(String uri,Class<T> clazz){
            return RemoteOperatorManager.get().putRemoteUri(uri,clazz);
        }
    
        /**
         * 启用路由地址
         * @param ctx Context
         * @param uri 路由地址
         * @return
         */
        public static <V> V invoke(Context ctx, String uri){
            if (checkUri(uri)){
                IOperator<?, V> operator = RemoteOperatorManager.get().getOperator(uri);
                return operator.invoke(ctx,uri);
            }else {
                throw new NotFoundRuleException(uri);
            }
        }
    
        /**
         * 路由地址检查
         * @param uri 路由地址
         * @return
         */
        public static boolean checkUri(String uri){
            return RemoteOperatorManager.get().checkOperatorForURI(uri);
        }
    
    }
    
    (六)测试
    • 在Application中添加、注册路由
    public class RemoteApp extends Application {
        @Override
        public void onCreate() {
            super.onCreate();
            initRemote();
        }
    
        private void initRemote() {
            Remote.putRemoteUri(ActivityIntentOperator.PROTOCOL+ IRemoteUrlConfig.LOGIN_REMOTE_URL, LoginActivity.class);
    
        }
    }
    
    • 在MainActivity中调用,并执行跳转
    Intent invoke = Remote.invoke(MainActivity.this, ActivityIntentOperator.PROTOCOL + IRemoteUrlConfig.LOGIN_REMOTE_URL);
     startActivity(invoke);
    

    至此,我们已经完成了一个路由框架,已经可以解决多Module之间,页面跳转问题。


    (三)拔高与提升

    如果已经消化完上面内容,那么就可以再跟随我们做一些拔高与提升了。
      下面我们将会使整个框架提升到通过注解Annotation的形式,来完成所有的工作,达到简化使用的目的。

    1、创建RemoteAnnotation Module
    • 创建注解@Module——指明当前页面所在Module
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.CLASS)
    public @interface Module {
       String value()default "";
    }
    
    • 创建注解@Modules——指明当前有多少个Module
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.CLASS)
    public @interface Modules {
        String[]value();
    }
    
    2、创建RemoteCompiler Module
    • 创建配置文件
    public class Config {
        public static final String PACKAGE_NAME = "com.mmyz.router";
        public static final String PAGE_PREFIX = "Module_";
        public static final String CLASS_NAME = "AutoRegisterRemote";
        public static final String METHOD_NAME = "autoRegister";
        public static final String PAGE_METHOD_NAME = "autoInvoke";
    }
    
    @AutoService(Processor.class)
    public class RemoteProcessor extends AbstractProcessor {
    
        private Filer mFiler;
        private Messager mMessager;
    
        private List<String> mStaticRemoteUriList = new ArrayList<>();
    
        @Override
        public synchronized void init(ProcessingEnvironment processingEnvironment) {
            super.init(processingEnvironment);
            //文件操作
            mFiler = processingEnvironment.getFiler();
            //消息输出
            mMessager = processingEnvironment.getMessager();
        }
    
        /**
         * 当前java 源码版本
         * java compiler version
         * @return
         */
        @Override
        public SourceVersion getSupportedSourceVersion() {
            return SourceVersion.latest();
        }
    
        /**
         * 指明需要关心的注解
         * need handle Annotation type
         * @return
         */
        @Override
        public Set<String> getSupportedAnnotationTypes() {
            Set<String> types = new HashSet<>();
            types.add(Modules.class.getCanonicalName());
            types.add(Module.class.getCanonicalName());
            types.add(StaticRemote.class.getCanonicalName());
            return types;
        }
    
        /**
         * 具体处理
         * @param set
         * @param re
         * @return
         */
        @Override
        public boolean process(Set<? extends TypeElement> set, RoundEnvironment re) {
            //清除URL维护集合
            mStaticRemoteUriList.clear();
            try {
                Set<? extends Element> modules = re.getElementsAnnotatedWith(Modules.class);
                if (modules != null && !modules.isEmpty()){
                    patchModulesClass(modules);
                    return true;
                }
                processModule(re);
    
            }catch (Exception e){
                mMessager.printMessage(Diagnostic.Kind.NOTE,e.getMessage());
            }
            return true;
        }
        
        private void processModule(RoundEnvironment re) {
    
            try {
                Set<? extends Element> staticElementSet = re.getElementsAnnotatedWith(StaticRemote.class);
                if (staticElementSet != null && !staticElementSet.isEmpty()) {
                    for (Element e :
                            staticElementSet) {
                        if (!(e instanceof TypeElement)) {
                            continue;
                        }
                        TypeElement te = (TypeElement) e;
                        mStaticRemoteUriList.add(te.getAnnotation(StaticRemote.class).value());
                    }
                }
    
                Set<? extends Element> module = re.getElementsAnnotatedWith(Module.class);
    
                patchModuleClass(module);
            }catch (Exception e) {
                mMessager.printMessage(Diagnostic.Kind.NOTE,e.getMessage());
            }
        }
    
        /**
         * 创建class文件
         * create class
         *
         *  package com.mmyz.router;
         *
         *  public class Page_Login(){
         *      public static autoInvoke(){
         *          Remote.putRemoteUriDefaultPattern("activity://com.mmyz.account.LoginActivity");
         *      }
         *  }
         *
         */
        private void patchModuleClass(Set<? extends Element> module) {
    
            try {
                if (module == null || module.isEmpty())
                    return;
    
                mMessager.printMessage(Diagnostic.Kind.NOTE,module.toString());
                Element next = module.iterator().next();
                Module annotation = next.getAnnotation(Module.class);
    
                String pageName = annotation.value();
                String className = Config.PAGE_PREFIX+pageName;
    
                JavaFileObject file = mFiler.createSourceFile(className, next);
    
                PrintWriter printWriter = new PrintWriter(file.openWriter());
                printWriter.println("package "+ Config.PACKAGE_NAME +";");
                printWriter.println("import "+ Config.PACKAGE_NAME+".Remote;");
                printWriter.println("import "+ Config.PACKAGE_NAME+".exception.NotFoundClassException;");
                printWriter.println("public class "+className +" {");
                printWriter.println("public static void "+ Config.PAGE_METHOD_NAME+"(){");
    
                printWriter.println("try{");
                for (String uri :
                        mStaticRemoteUriList) {
                    printWriter.println("Remote.putRemoteUriDefaultPattern(\""+uri+"\");");
                }
                printWriter.println("}catch(NotFoundClassException e){");
                printWriter.println("e.printStackTrace();");
                printWriter.println("}");
                printWriter.println("}");
                printWriter.println("}");
                printWriter.flush();
                printWriter.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
    
        }
    
        /**
         * 创建class文件
         * Create class
         *
         *    package com.mmyz.remote;
         *    public class AutoRegisterRemote{
         *        public void autoRegister(){
         *            Page_Login.autoInvoke();
         *        }
         *    }
         */
        private void patchModulesClass(Set<? extends Element> modules) {
    
            try {
                TypeElement moduleTypeElement= (TypeElement) modules.iterator().next();
                JavaFileObject file = mFiler.createSourceFile(Config.CLASS_NAME, moduleTypeElement);
                PrintWriter writer = new PrintWriter(file.openWriter());
                writer.println("package "+ Config.PACKAGE_NAME+";");
                writer.println("public class "+ Config.CLASS_NAME +" {");
                writer.println("public static void "+ Config.METHOD_NAME +" () {");
                Modules modulesAnnotation = moduleTypeElement.getAnnotation(Modules.class);
                String[] value = modulesAnnotation.value();
    
                for (String item :
                        value) {
                    writer.println(Config.PACKAGE_NAME+"."+ Config.PAGE_PREFIX+item+"."+ Config.PAGE_METHOD_NAME +"();");
                }
                writer.println("}");
                writer.println("}");
                writer.flush();
                writer.close();
            } catch (IOException e) {
                e.printStackTrace();
                mMessager.printMessage(Diagnostic.Kind.ERROR,e.getMessage());
            }
        }
    }
    
    • 创建反射调用类
    public class RemoteRegister {
        public static void register(){
            try {
                Class<?> clazz = Class.forName(Config.PACKAGE_NAME + "." + Config.CLASS_NAME);
                Method method = clazz.getDeclaredMethod(Config.METHOD_NAME);
                method.invoke(null);
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            } catch (NoSuchMethodException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    }
    - 改进Remote类——添加两个自动处理方法
    
    /**
     * 根据默认规则自动解析Uri
     * @param uri 路由地址 Ac
     */
    public static RemoteOperatorManager putRemoteUriDefaultPattern(String uri) throws NotFoundClassException {
        // (activity://com.mmyz.account.LoginActivity)
        Pattern pattern = Pattern.compile("[/]+");
        String[] infos = pattern.split(uri);
        String protocol = infos[0];
        String page = infos[1];
        try {
            putRemoteUri(uri,Class.forName(page));
        } catch (ClassNotFoundException e) {
            throw new NotFoundClassException(page, uri);
        }
        return RemoteOperatorManager.get();
    }
    
    /**
     * Activity路由跳转
     * @param context Context
     * @param uri 路由地址
     * @param invokeCallback  调用回调,处理Intent传值
     */
    public static void startActivity(Context context,String uri,BaseInvokeCallback<Intent> invokeCallback){
        Intent intent = invoke(context, uri);
        intent = invokeCallback.invokeCallback(intent);
        if (intent != null){
            context.startActivity(intent);
        }else {
            throw new NotFoundIntentException();
        }
    }
    
    
    #####3、具体调用
    - 将两个Module编译到每个模块中
    
    //remoteannotation
    compile project(':remoteannotation')
    //remote
    compile project(':remote')
    //compiler
    compile project(':remotecompiler')
    
    - 在App模块的Application中调用注册
    

    @Modules({
    RemoteModuleConfig.ACCOUNT_MODULE,
    RemoteModuleConfig.PRODUCT_MODULE,
    RemoteModuleConfig.ORDER_MODULE})
    public class RemoteApp extends Application {
    @Override
    public void onCreate() {
    super.onCreate();
    initRemote();
    }

    private void initRemote() {
    

    // Remote.putRemoteUri(ActivityIntentOperator.PROTOCOL+ IRemoteUrlConfig.LOGIN_REMOTE_URL, LoginActivity.class);
    RemoteRegister.register();
    // try {
    // Remote.putRemoteUriDefaultPattern(ActivityIntentOperator.PROTOCOL+ LoginActivity.REMOTE_URL);
    // } catch (NotFoundClassException e) {
    // Log.e("=========",e.getMessage());
    // e.printStackTrace();
    // }
    }
    }

    - 在被需要的页面添加@Module和@StaticRemote注解
    

    @Module(RemoteModuleConfig.ACCOUNT_MODULE)
    @StaticRemote(ActivityIntentOperator.PROTOCOL+ RemoteUrlConfig.REGISTER_REMOTE_URL)
    public class RegisterActivity extends AppCompatActivity {

    
    - 调用
    
        btnProduct.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Remote.startActivity(
                        MainActivity.this,
                        ActivityIntentOperator.PROTOCOL + RemoteUrlConfig.PRODUCT_REMOTE_URL,
                        new BaseInvokeCallback<Intent>());
            }
        });
    
    ****
    ####四、总结
      通过多Module的划分,可以很好解决多人协作开发冲突问题,通过路由方式是很好的解决了,由于采用多Module划分,产生的耦合问题。并且让页面跳转逻辑更加清晰。所以相对来说本人还是比较推崇,因为无论是**解耦程度**,**可扩展性**,**可维护性**都得到了极大的**提升**。使用和学习成本都不高。当然目前这个是Demo项目用于讲解,如果真的想开发实际项目还需进一步完善,或者去搜集大手写的路由框架
    #### 实际代码请下载或者Frok项目,若果能给start那就万分感谢。
    #### [项目GitHub地址:https://github.com/ms-liu/AndroidRemoteDemo](https://github.com/ms-liu/AndroidRemoteDemo)
    ####欢迎大家给出中肯的建议和提高意见,你的鼓励将是我最大的动力。
    #### 个人邮箱:ms_liu163@163.com

    相关文章

      网友评论

      • xiaobinZh:原理分析的很透彻。 但是使用有点重,而且 会导致 InstantRun,失败。
        可以参考下 https://github.com/gybin02/RouterKit。 也是用了 APT,但是参考的是 Google
        auto-service 里面的代码,使用APT直接生成 配置文件。
      • 80ac41b8844a: 最近一直在考虑模块化,我把所有的公共类提取到了一个module,这样其他的所有都 compile project(':baselibrary'),然后app compile需要的module.,,我想问下.app compile的每一个module.都含有一个同一个basemodule,,这样会有问题吗,.
        80ac41b8844a:base module中有很多公用的jar,被多個引用后,会冲突吗,不是很明白,多个module下相同的jar会怎么样,.主要是module很多,basemodule就被引用了很多次
        Ms__Liu: @80ac41b8844a 一般不会,但是需要注意的是,jar包的提取,防止jar包冲突
      • unfind:我想表达的是,从demo中没有看出类似于模块化开发的那种路由意义。
        Ms__Liu: @unfind
        1、模块化开发是为了分工清晰,在测试时可以自由添加想要的模块。
        2、路由是为了解决模块化之后,不同模块间页面跳转问题。
        应该是我文章阐述不明,所以谢谢你的评论👍👍👍
      • unfind:我看了demo中,app依赖了所有的工程,这个路由还有什么意义?
        Ms__Liu: @unfind 但是你是可以自由去除你不需要的模块的呀,因为是Demo所以才加的全部

      本文标题:带你打造一个多Module路由框架

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