Android组件化总结
1、什么是组件化
组件化指将一个工程拆分为多个子Module,每个Module作为一个组件。组件内部功能单一,具有高复用性,可独立编译与打包运行,组件之间互相不依赖但可以互相通信,在最终打包时可按需求任意搭配组合各组件。
2、为什么要组件化
组件化能有效的提高工作效率,控制产品质量,主要体现在以下几个方面:
- 大幅提升编译速度。传统的工程架构,稍微改动一点代码就需要把整个工程编译一遍,而组件化后只需编译对应的单个组件,随着工程越来越大,组件化对编译速度的提升会越来越明显;
- 快速生成定制项目。由于组件的高复用、低耦合的特点,可以快速的将组件应用到其他APP工程中;
- 减少测试时间。添加修改功能时,不会牵一发而动全身,导致每次都需要做系统测试,只需要针对组件做测试即可保证产品质量;
- 提升协作效率。开发者只需专注自己开发的组件,不同组件可并行开发互不干扰,不会因为不同人员代码风格不统一而影响开发效率;
- 易维护,易扩展;清晰的工程结构,没有复杂的依赖关系,使得维护和扩展变得简单起来,能够从容应对快速变更的需求;
3、如何组件化
组件划分;
在Android组件化架构中,一般可以将组件分为三种类别:普通组件、App壳组件、Common组件。
普通组件
普通组件是按业务逻辑或按功能从项目中拆分出来的组件,普通组件之间互相不依赖。支持Module和Library两种工作模式。Module模式下组件作为单独的Application可以打包运行,Library模式下组件作为.aar包被App壳组件依赖使用。
如何从项目中拆分组件并没有一个确切的标准,建议先按照业务做一次粗粒度的划分,然后在根据情况按功能做一次细粒度的划分,这一工作最好由对整个项目比较熟悉的业务人员和架构人员共同协商定制。
Common组件
Common组件可以被其他多个组件共同依赖,用于存放一些通用的内容。如OkHttp、Glide、BaseXX类,Theme等。
App壳组件
App壳组件通常没有业务代码,只有一个Application类用于执行一些初始化操作,它不需要支持Library模式,其他组件在Library模式下都会被打包到此组件中。App包名、Application、buildType、签名、是否启用multiDex、选择依赖哪些组件等等都是在App壳组件中声明的。
组件模式切换;
前面已经介绍了组件的两种工作模式,一种是可以打包运行的Module模式,一种是作为.aar包被依赖使用的Library模式。在Gradle构建的工程中,可以添加一个isModule参数作为判断组件工作模式的标识,编译时只需修改此参数即可切换两种模式。
if (isModule.toBoolean()) { //Module模式,作为Application单独运行 apply plugin: 'com.android.application' } else { //Library模式,作为.aar包被主工程依赖 apply plugin: 'com.android.library' }
在Module模式下需要定义applicationId,versionCode,versionName等参数。
if (isModule.toBoolean()) { applicationId "com.gongw.login" versionCode 1 versionName "1.0" }
在Module模式下,组件的AndroidManifest.xml应该具有一个Android APP应有的所有属性,如Application、Launch Activity等。在Library模式下,组件的AndroidMainfest.xml会被合并到app壳组件中,为了避免冲突,就不能有Application、LaunchActivity这些属性,需要将这些排除在外。通常做法是定义两个AndroidManifest.xml文件,在build.gradle中用manifest.srcFile定义不同模式下的AndroidManifest.xml文件。
sourceSets { main { if (isModule.toBoolean()) { //Module模式下,使用module路径下的AndroidMainfest.xml文件 manifest.srcFile 'src/main/module/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' java { //在Library模式下,排除debug中存放的用于调试的代码 exclude 'debug/**' } } } }
组件间通信
组件间通信一般涉及UI跳转、相互调用、数据传输等常见操作,在组件化架构中,因为组件之间没有依赖关系,无法使用显式intent跳转界面,无法直接调用其他组件中的方法,也无法直接传输数据。这种情况下一般需要一个中间层来处理请求信息,比如使用隐式Intent跳转界面,中间层就是系统提供的PackageManagerService,比如使用广播就是用系统提供的ActivityManagerService作为中间层,其他的还有AIDL,总线等。而为了方便我们可以直接使用开源方案,如ARouter,ActivityRouter,ModuleBus等。
Library库重复依赖;
使用第三方库的过程中,经常会遇到多个库重复依赖的问题,需要将多余的包排除出去,可以参考如下配置。
api "com.squareup.okhttp3:okhttp:3.9.1" api ('com.squareup.retrofit2:retrofit:2.4.0'){ //移除已经依赖过的okhttp exclude module: 'okhttp' exclude module: 'okio' } api "io.reactivex.rxjava2:rxjava:2.1.8" api ('io.reactivex.rxjava2:rxandroid:2.0.2'){ //移除已经依赖过的rxjava exclude module: "rxjava" }
资源id冲突;
当不同组件存在相同的资源id时会导致资源的冲突,而且编译的时候还不会报错,在运行的时候才会暴露出来。gradle提供了resourcePrefix参数解决这个问题,它规定组件中的资源名称必须带有指定的前缀,否则编译无法通过。
//限制资源名称必须带有login_前缀 resourcePrefix "login_"
注:这个方法无法限制像图片这种不在代码中命名的资源,只能靠开发者自觉规范命名;
全局Context
通常为了使用全局Context,需要获取application对象,在Module模式下每个组件都可以有自己的Application,而在Library模式下,除了App壳组件,其他组件的Application会被剔出,以保证一个程序只有一个Application。为了避免组件切换到Library模式后出现错误,组件中使用的全局Context必须保证在Module和Library模式下都有效,不能直接使用自己声明的Application。
为了解决这一问题,通常的做法是在Common组件中定义一个BaseApplication类,用于各组件继承用,这样应用启动时BaseApplication就会被实例化,各组件只需要使用从BaseApplication获取的Context就能保证在两种模式下都能使用。
统一管理依赖库版本
为了避免不同组件依赖版本不一致,出现兼容性的问题。推荐将组件中依赖库的版本号放到一个文件中统一管理,通常放在项目最外层的build.gradle中,这个文件中定义的常量能被整个项目中的所有build.gradle文件引用。
ext { // Sdk and tools buildToolsVersion = 26.0.2 compileSdkVersion = 26 minSdkVersion = 16 targetSdkVersion = 22 versionCode = 1 versionName = "1.0.0" javaVersion = JavaVersion.VERSION_1_7 // App dependencies version supportLibraryVersion = "26.1.0" multidexVersion = "1.0.2" //第三方库版本 arouterApiVersion = "1.3.1" annotationProcessor = "1.1.4" glideVersion = "3.7.0" gsonVersion = "2.8.2" rxjavaVersion = '2.1.8' rxandroidVersion = '2.0.2' okhttpVersion = '3.9.1' }