Android从0到1实现模块化开发,封装(MVP、Retrofit2、Rxjava2、Arouter等)组合框架

题外话:这一两年来随着人工智能的火爆,越来越多的人都去做AI、人工智能什么的,移动互联网的风口位置也已让贤,但是不在风口,我们也得坚持安卓下去,不是什么火我们就往那里拱(拱 二声,四川话:去),新技术总有一天也会失去众人的焦点被其他新事物所取代,所以坚持下去,我们会做的更好的。

前言:在我们日常开发中,当项目业务逐渐变得多起来,在app目录下怼代码感觉会越来越臃肿,有时还会耦合的很严重,最要命最痛苦的是编译的时候,一个小改动改个字符串什么的可能几秒钟就完成了,但等待编译却要好几分钟,更甚至十几分钟,这肯定是不能忍的,毕竟我们工作时间很宝贵,能不拖时间就不拖,免得影响下班时间,对吧。所以这个时候,安卓模块化就显得格外重要了。

注意:这里只着重讲怎么去架构,还有常用的思路方法,深层次的原理等不大会涉及到,如果大家有需要看原理什么的就去看其他大牛的讲解哦!

一、模块化解决的问题:

1.各个子module间互不依赖,形成解耦。

2.各个模块作为application时可以单独运行,作为library时,只提供给app模块依赖打包使用。

3.便于各个开发人员间的合作开发。

4.各个模块作为application单独编译时间减少,程序员心情值up。

下面将根据demo来讲解具体用法:

二、首先看到android目录架构

包括上方红框的基础组件lib_common以及lib_opensource,lib_opensource作用是所有三方库的一个依赖组件,lib_opensource只被lib_common依赖。lib_common作用是提供所有base类,包括但不限于封装的mvp、网络框架、基本配置类、utils、一些公共的轻量业务。当然如果说业务具有登录注册的,那么也推荐放到lib_common里面,当我们单独编译某一个模块又必须要用到登录时,那么此时所有业务子模块就直接依赖lib_common就足够了,当我们需要整体编译打包时,app模块只需要依赖各个业务子模块,不需要单独去依赖lib_common。

1.怎么去配置是否单独编译还是整体打包编译:

我们需要去定义一个全局的常量,让我们各个子模块自动去识别这个常量就行。以往我们定义一个全局常量可能就只需要一个静态类就搞定,那么对于整个工程来说有没有一个常量能够被所有module所识别呢?答案是肯定的,Gradle自动构建工具有一个重要属性,可以帮助我们完成这个事情。每当我们用AndroidStudio创建一个Android项目后,就会在项目的根目录中生成一个文件 gradle.properties,我们将使用这个文件的一个重要属性:在Android项目中的任何一个build.gradle文件中都可以把gradle.properties中的常量读取出来;那么我们在上面提到解决办法就有了实际行动的方法,首先我们在gradle.properties中定义一个常量值 isBuildModule(是否是组件开发模式,true为是,false为否),在项目根目录的 gradle.properties 中定义变量:

这里需要注意的是,gradle.properties 中的数据类型都是String类型,使用其他数据类型需要自行转换,也就是说我们读到 isBuildModule是个String类型的值,而我们需要的是Boolean值,所以需要在子模块的build.gradle 使用的地方中加入红框代码:

上面的isBuildModule 为true表示,子模块会以application存在,即可单独打包编译等,当为false时,作为library供application依赖使用。

此时还需要注意两点:

1)当子模块作为application时有自己的包名,需要这个字段。而当子模块作为library时,子模块的build.gradle中不需要包名applicationId这个字段,我们同样根据isBuildModule字段来判断:

2)manifest合并问题。当子模块作为library时,是由app主模块启动的,此时子模块的manifest里面不需要配置launch也不需要配置application的label、icon等。而子模块作为application时是自己本身启动,需要在manifest配置launch以及application等。此时的清单文件不能同时满足两个条件,那么我们就通过配置两个不同的清单文件release版和debug版来解决这个问题。

在子module的main目录下新建两个文件夹,并将清单文件放入其中,如下:

             

debug 模式下的 AndroidManifest.xml :

<application
   ...
   >
   <activity
       android:name="com.senon.module_one.MainActivity"
       android:label="module_one">
       <intent-filter>
           <action android:name="android.intent.action.MAIN" />
           <category android:name="android.intent.category.LAUNCHER" />
       </intent-filter>
   </activity>
</application>

realease 模式下的 AndroidManifest.xml :

<application
   ...
   >
   <activity
       android:name="com.senon.module_one.MainActivity"
       android:label="module_one">
       <intent-filter>
           <category android:name="android.intent.category.DEFAULT" />
           <category android:name="android.intent.category.BROWSABLE" />
           <action android:name="android.intent.action.VIEW" />
           <data android:host="com.senon.module_one"
               android:scheme="router" />
       </intent-filter>
   </activity>
</application>

当写好了两个不同版本的清单文件之后,我们需要在对应的每个子模块的build.gradle去配置不同版本的清单文件

如果项目中出现
No signature of method: static org.gradle.api.java.archives.Manifest.srcFile() is applicable for argument types: (java.lang.String) values: [src/main/debug/AndroidManifest.xml]时,是因为manifest写成了Manifest,把大写改成小写就ok了。

2.三方依赖库如何使用:

上面提到的lib_opensource模块是专门存放三方开源等依赖,使用方法一般是在根目录新建一个gradle脚本文件,我们命名为dependencies.gradle,我们把所有的三方依赖全部放在里面,并定义新的名称方便调用,如下:

划重点:然后必须在项目工程根build.gradle中去应用这个dependencies.gradle

接下来,lib_common的build.gradle中依赖lib_opensource这个模块,如下:

注意到上面红框里面了吗? 依赖用的是api 而不是implementation。为什么尼?

原因是安卓3.0以后google为了安卓的规范,而使用了api和implementation来替代以前的compile,api和以前的compile作用是一样的,而implementation的出现是为了依赖私有化(我个人这样理解的)。如果B implementation了A,那么B可以调用到A中的api接口(也就是A中的各种类,接口之类的),此时如果C implementation了B,那么C是不能够调用到A中的api接口的,所以说implementation是保护了A中的api接口的,这样做增加了项目的构建速度,非常棒。

回过来,如果我们想要C也可以调用A的接口,怎么办尼?这时就应该用api了,B api A,C implementation B,此时C就能够调用A的api接口了。现在发散下思维(偷笑),如果D implementation C,D想调用A中的api接口怎么办?答案是B api A,C api B,D implementation/api C。

3.资源名称命名

在模块化中,会出现这种情况,主module依赖了module_one、module_two,one、two同时又一张叫logo的图片,主module需要调用two中的logo图片,调用并运行程序,此时打开app一看,发现logo显示的不是two中的那一张,程序也没有报错,这是怎么回事呢?原因是程序不知道到底要调用哪一个logo图,所以调用出现混乱。

解决这个的办法,使用resourcePrefix约束资源前缀,来防止冲突,此时在所有子模块的build.gradle的android下增加各自的资源前缀 ,一般使用module+下划线_组成,比如one_

此时我们子模块下面res文件夹下面的所有文件命名全部要用one_为前缀,比如one_activity_main.xml,包括layout、value、drawable、mipmap等里面的所有文件。

 

三、模块间的跳转问题

由于子模块之间是禁止互相依赖的,所以他们之间不能跳转、传值等,一般来说解决这个问题有几种方法,一是隐式跳转,但不方便传值及后期维护修改,二是最常用的路由跳转,我们项目中所用的路由跳转为阿里巴巴开源ARouter,使用起来比较简单:

1,添加依赖和配置

android {
    defaultConfig {
        ...
        javaCompileOptions {
            annotationProcessorOptions {
                arguments = [AROUTER_MODULE_NAME: project.getName()]
            }
        }
    }
}
dependencies {
    // x.x.x替换成最新版本, 需要注意的是api
    // 要与compiler匹配使用,均使用最新版可以保证兼容
    compile 'com.alibaba:arouter-api:x.x.x'
    annotationProcessor 'com.alibaba:arouter-compiler:x.x.x'
    ...
}

2,初始化路由,一般在Application中。

if (isDebug()) {           // 这两行必须写在init之前,否则这些配置在init过程中将无效
    ARouter.openLog();     // 打印日志
    ARouter.openDebug();   // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(mApplication); // 尽可能早,推荐在Application中初始化

3,定义为被跳转的目标页面注册路由:

一般的路由地址由两级或多级构成,我们可以统一规定路径地址为 /模块名/包名/类名,也可以直接定义为  /模块名/类名,只要类名不重复就ok,具体如何使用请查看ARouter官方文档。

我这里要讲的是如何封装路由地址,封装过后对以后的迭代有帮助。我们在lib_common建一个常量类,所以路由跳转均写入本ConstantArouter方便调用, 我们规定路由跳转命名统一用:path+模块名+Activity名

我们在目标页面注册路由就可以改为 @Route(path = ConstantArouter.PATH_APP_FRAGMENTHOMEACTIVITY)

注意:如果出现如下错误

com.android.dex.DexException: Multiple dex files define Lcom/alibaba/android/arouter/routes/ARouter$$Group$$module

运行时出这个异常是因为不同模块有相同分组导致的,例如AMoudle定义了@Route(path = “/module/**”),BMoudle也定义了@Route(path = “/module/***)就会出现这个问题。

附上demo的github地址 https://github.com/senonwx/ModularApp

最后我也用此架构完成了玩安卓app,github地址 https://github.com/senonwx/WanAndroid

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章