模块化开发项目搭建
1.为什么要模块化开发
随着APP版本不断的迭代,新功能的不断增加,业务也会变的越来越复杂,APP业务模块的数量有可能还会继续增加,而且每个模块的代码也变的越来越多,这样发展下去单一工程下的APP架构势必会影响开发效率,增加项目的维护成本,每个工程师都要熟悉如此之多的代码,将很难进行多人协作开发,而且Android项目在编译代码的时候电脑会非常卡,又因为单一工程下代码耦合严重,每修改一处代码后都要重新编译打包测试,导致非常耗时,最重要的是这样的代码想要做单元测试根本无从下手,所以必须要有更灵活的架构代替过去单一的工程架构。
2.如何模块化
名词 含义
集成模式 所有的业务组件被“app壳工程”依赖,组成一个完整的APP;
组件模式 可以独立开发业务组件,每一个业务组件就是一个APP;
app壳工程 负责管理各个业务组件,和打包apk,没有具体的业务功能;
业务组件 根据公司具体业务而独立形成一个的工程;
功能组件 提供开发APP的某些基础功能,例如打印日志、树状图等;
Main组件 属于业务组件,指定APP启动页面、主界面;
Common组件 属于功能组件,支撑业务组件的基础,提供多数业务组件需要的功能,例如提供网络请求功能;
3、模块化实施流程
1.设置组建模式和集成模式
1. 创建项目1. 创建一个app的module,也就是main组件2. 基本类库commonlib,存放所有模块共用工具,也就是common组件3. 创建依赖包及版本管理文件version.gradle1. 创建gradle文件2. 然后在项目的build.gradle中添加version.gradle的全局配置apply from: 'version.gradle3. ext是自定义属性,把所有关于版本的信息都利用ext放在另一个自己新建的gradle文件中集中管理//版本信息def build_version=[:]build_version.min_sdk=16build_version.target_sdk = 28build_version.build_tools = "27.0.3"ext.build_version = build_versionext.deps = deps4. 创建module模块如:ccplay模块、other模块
2. 设置application属性和library属性1. 我们在gradle.properties中定义一个常量值 isModule(是否是组件开发模式,true为是,false为否),/**在Android项目中的任何一个build.gradle文件中都可以把gradle.properties中的常量读取出来**/2. 然后我们在业务组件的build.gradle中读取 isModule,但是 gradle.properties 还有一个重要属性: gradle.properties 中的数据类型都是String类型,使用其他数据类型需要自行转换;也就是说我们读到 isModule 是个String类型的值,而我们需要的是Boolean值,代码如下:if (isModule.toBoolean()) {apply plugin: 'com.android.application'} else {apply plugin: 'com.android.library'} 3. 当每次改变isModule的值后,都要同步项目才能生效
3. 修改依赖包到共用模块commonlib中dependencies {implementation fileTree(dir: 'libs', include: ['*.jar'])testImplementation 'junit:junit:4.12'api deps.support.v7api deps.support.constraintapi deps.support.multidex}
4. 各个模块中依赖使用公共模块,在模块的build.gradle中添加dependenciesimplementation project(':commonlib')
2.处理组件之间AndroidManifest合并问题
问题
开发时 AndroidManifest.xml 文件的位置是不一样的,我们需要在build.gradle 中指定下 AndroidManifest.xml 的位置,AndroidStudio 才能读取到 AndroidManifest.xml,
解决方法
我们可以为组件开发模式下的业务组件再创建一个 AndroidManifest.xml,然后根据isModule指定AndroidManifest.xml的文件路径,让业务组件在集成模式和组件模式下使用不同的AndroidManifest.xml,这样表单冲突的问题就可以规避了
步骤
1. 在子组件的main文件下创建目录module,添加一个清单文件
2. 在子组件的build.gradle中添加配置,在集成模式和组件模式下分别取不同的清单文件的使用sourceSets {main {if (isModule.toBoolean()) {manifest.srcFile 'src/main/module/AndroidManifest.xml'//集成开发模式下排除debug文件夹中的所有Java文件java {exclude 'debug/**'}} else {manifest.srcFile 'src/main/AndroidManifest.xml'}}}
3.整理不同的清单文件的配置**原因**首先是集成开发模式下的 AndroidManifest.xml,前面我们说过集成模式下,业务组件的表单是绝对不能拥有自己的 Application 和 launch 的 Activity的,也不能声明APP名称、图标等属性,总之app壳工程有的属性,业务组件都不能有**处理完成**module目录下的清单文件内容如下<applicationandroid:theme="@style/AppTheme"><activity android:name=".MainActivity"></activity></application>**注意**在清单文件中声明了主题,而且这个主题还是跟app壳工程中的主题是一致的,都引用了common组件中的资源文件,在这里声明主题是为了方便这个业务组件中有使用默认主题的Activity时就不用再给Activity单独声明theme了外面main目录下的清单文件内容如下<applicationandroid:allowBackup="true"android:icon="@mipmap/ic_launcher"android:label="@string/app_name"android:roundIcon="@mipmap/ic_launcher_round"android:supportsRtl="true"android:theme="@style/AppTheme"><activity android:name=".MainActivity"><intent-filter><action android:name="android.intent.action.MAIN" /><category android:name="android.intent.category.LAUNCHER" /></intent-filter></activity></application>
3.全局Context的获取及组件数据初始化
当Android程序启动时,Android系统会为每个程序创建一个 Application 类的对象,并且只创建一个,application对象的生命周期是整个程序中最长的,它的生命周期就等于这个程序的生命周期。在默认情况下应用系统会自动生成 Application 对象,但是如果我们自定义了 Application,那就需要在 AndroidManifest.xml 中声明告知系统,实例化的时候,是实例化我们自定义的,而非默认的。
但是我们在模块化开发的时候,可能为了数据的问题每一个组件都会自定义一个Application类,如果我们在自己的组件中开发时需要获取 全局的Context,一般都会直接获取 application 对象,但是当所有组件要打包合并在一起的时候就会出现问题,因为最后程序只有一个 Application,我们组件中自己定义的 Application 肯定是没法使用的,因此我们需要想办法再任何一个业务组件中都能获取到全局的 Context,而且这个 Context 不管是在组件开发模式还是在集成开发模式都是生效的。
在模块化工程模型图中,功能组件集合中有一个 Common 组件, Common 有公共、公用、共同的意思,所以这个组件中主要封装了项目中需要的基础功能,并且每一个业务组件都要依赖Common组件,Common 组件就像是万丈高楼的地基,而业务组件就是在 Common 组件这个地基上搭建起来我们的APP的,Common 组件会专门在一个章节中讲解,这里只讲 Common组件中的一个功能,在Common组件中我们封装了项目中用到的各种Base类,这些基类中就有BaseApplication 类。
BaseApplication 主要用于各个业务组件和app壳工程中声明的 Application 类继承用的,只要各个业务组件和app壳工程中声明的Application类继承了 BaseApplication,当应用启动时 BaseApplication 就会被动实例化,这样从 BaseApplication 获取的 Context 就会生效,也就从根本上解决了我们不能直接从各个组件获取全局 Context 的问题;
这时候大家肯定都会有个疑问?不是说了业务组件不能有自己的 Application 吗,怎么还让他们继承 BaseApplication 呢?其实我前面说的是业务组件不能在集成模式下拥有自己的 Application,但是这不代表业务组件也不能在组件开发模式下拥有自己的Application,其实业务组件在组件开发模式下必须要有自己的 Application 类,一方面是为了让 BaseApplication 被实例化从而获取 Context,还有一个作用是,业务组件自己的 Application 可以在组件开发模式下初始化一些数据,例如在组件开发模式下,A组件没有登录页面也没法登录,因此就无法获取到 Token,这样请求网络就无法成功,因此我们需要在A组件这个 APP 启动后就应该已经登录了,这时候组件自己的 Application 类就有了用武之地,我们在组件的 Application的 onCreate 方法中模拟一个登陆接口,在登陆成功后将数据保存到本地,这样就可以处理A组件中的数据业务了;另外我们也可以在组件Application中初始化一些第三方库。
但是,实际上业务组件中的Application在最终的集成项目中是没有什么实际作用的,组件自己的 Application 仅限于在组件模式下发挥功能,因此我们需要在将项目从组件模式转换到集成模式后将组件自己的Application剔除出我们的项目;在 AndroidManifest 合并问题小节中介绍了如何在不同开发模式下让 Gradle 识别组件表单的路径,这个方法也同样适用于Java代码;
处理步骤 1. 在module目录下创建一个debug文件,用于存放不再组件模式下引用的类,例如application,也可以在 debug 文件夹中创建一个Activity,然后组件表单中声明启动这个Activity,在这个Activity中不用setContentView,只需要在启动你的目标Activity的时候传递参数就行,这样就就可以解决组件模式下某些Activity需要getIntent数据而没有办法拿到的情况,代码如下
public class LauncherActivity extends AppCompatActivity {@Override
protected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);request();Intent intent = new Intent(this, TargetActivity.class);intent.putExtra("name", "avcd");intent.putExtra("syscode", "023e2e12ed");startActivity(intent);finish();
}//申请读写权限
private void request() {AndPermission.with(this).requestCode(110).permission(Manifest.permission.WRITE_EXTERNAL_STORAGE,Manifest.permission.CAMERA, Manifest.permission.READ_PHONE_STATE).callback(this).start();
}}
-
在commonlib下创建基础的BaseApplication,然后让各个模块继承
public class BaseApplication extends MultiDexApplication {private static BaseApplication instance;@Override public void onCreate() {super.onCreate();instance = this; }public static BaseApplication getInstance(){return instance; } }
-
各个模块的debug目录下的application中添加BaseApplication的继承,例如
public class OtherAplication extends BaseApplication { }
-
因为在集成模式下我们使用统一的一个application,所以需要在集成模式下过滤掉模块的application,和一些组件模式下的文件类。在每个组件的build.gradle的sourceSets中过滤掉debug目录下的文件类
java {exclude 'debug/**'}
4.依赖包文件的处理
原因
- 每个项目模块都会有很多的依赖包,管理很不方便
- 每个子模块都依赖相同的包也很多,需要统一处理
解决方式
- 创建一个公共的依赖包管理文件,上面提到的version.gradle
- 在文件中定义依赖的类库,及版本号码,自定义ext
- 在项目的build.gradle中添加version类库的全局使用
- 在基础commonlib模块中添加需要的使用的类库
- 每个组件都依赖commonlib,也就相当于依赖了它的类库,方便使用用。
5.组件之间的调用和通信
项目间的通信跳转,我们使用的是阿里巴巴的开源路由项目ARouter
6.模块化时资源名冲突
资源名冲突有哪些?
比如,color,shape,drawable,图片资源,布局资源,或者anim资源等等,都有可能造成资源名称冲突。这是为何了,有时候大家负责不同的模块,如果不是按照统一规范命名,则会偶发出现该问题。 尤其是如果string, color,dimens这些资源分布在了代码的各个角落,一个个去拆,非常繁琐。其实大可不必这么做。因为android在build时,会进行资源的merge和shrink。res/values下的各个文件(styles.xml需注意)最后都只会把用到的放到intermediate/res/merged/../valus.xml,无用的都会自动删除。并且最后我们可以使用lint来自动删除。所以这个地方不要耗费太多的时间。
解决办法
这个问题也不是新问题了,第三方SDK基本都会遇到,可以通过设置 resourcePrefix 来避免。设置了这个值后,你所有的资源名必须以指定的字符串做前缀,否则会报错。但是 resourcePrefix 这个值只能限定 xml 里面的资源,并不能限定图片资源,所有图片资源仍然需要你手动去修改资源名。
个人建议
将color,shape等放到基础库组件中,因为所有的业务组件都会依赖基础组件库。在styles.xml需注意,写属性名字的时候,一定要加上前缀限定词。假如说不加的话,有可能会在打包成aar后给其他模块使用的时候,会出现属性名名字重复的冲突,为什么呢?因为BezelImageView这个名字根本不会出现在intermediate/res/merged/../valus.xml里, 所以不要以为这是属性的限定词!
问题
1.使用butterknife时运行版本不支持问题
Error:Static interface methods are only supported starting with Android N (--min-api 24): void butterknife.Unbinder.lambda$static$0()
Error:Invoke-customs are only supported starting with Android O (--min-api 26)
Error:com.android.builder.dexing.DexArchiveBuilderException: Failed to process C:\Users\Administrator\.gradle\caches\transforms-1\files-1.1\butterknife-runtime-9.0.0-SNAPSHOT.aar\ccbbf27ed1b4a4afbd13c27afe9e6d15\jars\classes.jar
解决方式:可以通过在app的build.gradle文件中配置使用java8编译:
android {...compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}
}
2.v4包的依赖
Error:Program type already present: android.support.v4.app.FrameMetricsAggregator$FrameMetricsBaseImpl
解决方式使用java8编译
在module的build.gradle中defaultconfig中添加compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}
3.关于多模块资源文件重名的问题
java.lang.NoSuchFieldError: No field textView of type I in class Lcom/ncr/ncrs/other/R$id; or its superclasses (declaration of 'com.ncr.ncrs.other.R$id' appears in /data/app/com.ncr.ncrs.ncrstudent-2/base.apk)
解决方式:修改每个模块的资源文件,不要有相同命名的文件,给每个资源文件前加一个module的前缀