「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。
为什么使用组件化
-
单工程项目问题
- 随着业务迭代代码越来越臃肿。
- 代码碎片化验证,出现bug,难以很快定位问题,维护困难。
- 对于新功能的开发,避免改动影响原有业务,导致开发成本不断增加。
- 项目编译时间长,中大型项目编译时间动辄10几分钟,开发效率低。
- 代码复用性差,功能代码耦合了大量业务代码,难以拆分复用。
- 不易于团队开发,功能耦合严重,边界不明确,代码合并经常发生冲突。
-
组件化优势
- 模块解耦,对各业务、功能模块进行分离,每个模块都是一个独立的组件,可独立运行(业务模块)。
- 代码复用,模块解耦之后也就意味着高度可复用性。
- 加快编译速度,相对于单工程项目,组件化每个业务模块可独立编译运行,大大加快了编译速度。
- 易于团队合作开发,各个组件指定人员负责开发,各组件之间没有耦合,功能边界清晰,可以有效降低沟通成本。
模块化、组件化和插件化及差别。
- 模块化,更倾向于对独立的功能进行模块化封装,如:网络访问、图片加载和log库等,但对于业务模块的解耦不彻底,依旧容易出现各业务模块高度耦合的问题。
- 组件化,各功能模块、业务模块高度解耦互不影响,各业务模块之间没有直接相互引用,之间数据交互通过路由实现。
- 插件化,组件化注重编译时的动态配置项目,插件化注重运行时动态配置。组件化编译安装后功能确定,不可动态变更,插件化编译安装后,可以动态加载功能模块。可以说组件化是插件化的基础,插件化是通过一些动态加载机制,使得各组件代码(通常是aar或dex)可以在apk被安装运行后动态进行加载。
组件化开发过程
项目组件及层级划分
组件划分是组件化开发中最重要的一步,通常会按照功能和业务将项目进行分层设计。如下图:
-
library层
提供通用功能,如网络、图片加载和log库等,通常是基于第三方开源方案做的二次封装库,因为修改频率低也可以作为aar或远程仓库引用。
-
common层
提供基类的封装BaseActivity和BaseFragment等,并且引用所有的功能组件,为上层业务组件提供支持,而且同一维护所以基础组件有助于基本组件的版本同一。通常也将所以第三方库的引用放到此组件中,同一管理维护。
-
component层
业务组件层,将业务模块进行组件化拆分,隔离成功能完整的独立组件,各业务组件间禁止直接相互引用,各组件可配置独立调试、运行和测试。如:登录组件、注册组件、设置组件和用户中心组件等。
-
app壳
承载app的启动入口Activity和全局的Application等,没有任何复杂的业务逻辑代码,是引用各业务组件的“壳工程”。
各层级组件只能向下依赖,同层之间尽量避免直接引用依赖,越下层组件的功能越稳定,修改频率越小。
组件通信路由框架实现
上面提到了业务组件之间是项目隔离完全解耦的,那么组件之间页面的跳转,数据的传递,甚至服务的调用就变成了首要需要解决的问题,这就引入了路由的概念。
路由(routing)就是通过互联的网络把信息从源地址传输到目的地址的活动。路由发生在OSI网络参考模型中的第三层即[网络层])。
这是网络中路由概念的解释,在组件化开发中“路由”的概念与之类似,核心概念就是将目标页面与其对象解绑,传统的Android路由是通过Intent实现,Intent要求必须能够直接引用到目标页面类或者其Action,而路由是通过映射关系将目标页面与其“路由路径”相绑定,并维护统一的路由表,当发生页面跳转时,会从路由表中根据路径取出相应的目标页面对象进行跳转处理。(类似隐式Action跳转,但是相对于隐式跳转,不需要在Manifest中声明,可以统一管理路由表,更易于维护,且可以支持动态添加路由)
比较著名的路由框架有
- 阿里:ARouter(具体使用可以查看:ARouter入门使用)
- 美团:WMRouter
当然了处理页面Activity跳转,还有页面间数据传递,Fragment对象的获取,组件开放服务的调用等,这些上面的路由框架中都有提供相应的解决方案。
Gradle支持
组件化开发最大的特点就是各业务组件是可以独立运行调试的,但是在打包发布时又需要将所有的业务组件组装成一个app进行发布,为了实现这个功能就需要在Gradle进行相应的配置,来满足需求。
我们知道Android Studio是使用Gradle进行构建的,而Android Gradle插件提供了2种插件类型:application和library,对应的构建输出分别是apk和aar。在单组件调试时,业务组件是以application形式存在的,而在打包发布时业务组件是以library形式存在的。当然我们可以在调试和发布时手动修改组件的apply plugin,但是这样也太傻,太繁琐了。为了尽量简化这个过程,我们需要统一进行配置:
简单的组件化项目开发配置
-
在gradle.propertites配置统一变量
在项目根目录的gradle.propertites文件中定义的变量可以被项目中所有build.gradle访问。所以我们在文件中添加添加isModule变量,
true即为各组件独立调试模式,false是整合发布模式。android.useAndroidX=true # Automatically convert third-party libraries to use AndroidX android.enableJetifier=true # 模块是否独立运行开发 isModule=false -
修改build.gradle
之后我们就可以在业务组件的build.gradle中添加:
if (isModule.toBoolean()){ apply plugin: 'com.android.application' }else { apply plugin: 'com.android.library' } android{ //省略 } -
修改AndroidManifest
业务组件想要独立运行调试,除了需要将插件修改为
com.android.library外,还需要做的一件事是配置AndroidManifest.xml添加<application>节点,声明启动activity等,所以就需要两个AndroidManifest.xml文件来进行切换,需要通过gradle的sourceSets来根据不同模式加载不同的AndroidManifest文件。android{ //省略 sourceSets { main { if (isModule.toBoolean()) { manifest.srcFile 'src/main/module/AndroidManifest.xml' } else { manifest.srcFile 'src/main/AndroidManifest.xml' } } } }然后在项目的src/main目录下新建文件夹module添加
AndroidManifest.xml并正常添加组件运行需要的权限、和声明activity,并设置Activity的程序入口属性。<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.zhong.demo"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application android:allowBackup="true" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.GradleDemo"> <activity android:name=".MainActivity"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>而在组件正常的
AndroidManifest.xml中只需要声明运行权限并声明Activity(不需要设置app入口属性)。<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.zhong.demo"> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <application> <activity android:name=".MainActivity"/> </application> </manifest>
到这里最简单的组件化开发项目配置就基本完成了,就可以进入正式项目开发了。
Gradle配置统一管理
通过上面的步骤已经可以进行简单的组件化开发了,但是过于简单的配置依然可能存在一些问题:
- 资源文件冲突,各组件独立开发不可避免的会出现资源文件命名冲突问题,就到导致最终整合发布时的各种异常。
- 依赖库版本管理,组件独立开发可能出现资源库引用版本不一致的问题。
- 各组件相同的配置,需要配置多次。比如路由框架我们选用了阿里的ARouter,那么就需要在每一个组件的gradle文件中添加库应用并声明编译配置属性,未免太繁琐。
config.gradle
在项目根目录添加config.gradle用于统一管理编译环境和第三方库版本。
import java.text.SimpleDateFormat
ext {
//build
android = [
compileSdkVersion: 30,
buildToolsVersion: '30.0.2',
minSdkVersion : 19,
targetSdkVersion : 26,
applicationId : 'com.xxx.xxxx',
versionCode : 1,
versionName : "V1.0.0_1_${releaseTime()}"
]
//AndroidX
androidx = [
appcompat : 'androidx.appcompat:appcompat:1.1.0',
material : 'com.google.android.material:material:1.1.0',
constraintlayout: 'androidx.constraintlayout:constraintlayout:1.1.3',
multidex : 'androidx.multidex:multidex:2.0.1',
leanbackX : 'androidx.leanback:leanback:1.0.0'
]
//dependencies
dependencies = [
//gson
gson_version : '2.8.5',
//butterknife
butterknife_compiler_version: '10.2.2',
butterknife_version : '10.2.3',
//glide
glide_version : '4.10.0',
//retrofit
retrofit_version : '2.3.0',
converter_gson_version : '2.3.0',
//OkHttp
okhttp_version : '3.8.0',
logging_interceptor_version : '3.8.0',
//mmkv
mmkv_version : '1.2.11',
//ARouter
arouter_api_version : '1.5.2',
arouter_compiler_version : '1.5.2'
]
}
static def releaseTime() {
return new SimpleDateFormat("yyyyMMddHH").format(new Date())
}
然后在项目根目录的build.gradle中添加:
// Top-level build file where you can add configuration options common to all sub-projects/modules.
apply from: "config.gradle"
buildscript {
//省略
}
就可以在所有的gradle文件中使用了:
android {
compileSdkVersion rootProject.ext.android.compileSdkVersion
buildToolsVersion rootProject.ext.android.buildToolsVersion
defaultConfig {
applicationId rootProject.ext.android.applicationId
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode rootProject.ext.android.versionCode
versionName rootProject.ext.android.versionName
//省略
}
}
common.gradle
在根目录添加common.gradle用于统一维护所以组件配置项,同样需要先在gradle.properties中添加变量isModule=false。
然后在common.gradle针对组件的几种类型:application、library和可动态切换的组件,分别进行配置并对通用的配置项统一处理。
project.ext{
//设置app配置
setApplicationConfig = {
extension ->
//指定为application,代表该模块可以单独调试
extension.apply plugin: 'com.android.application'
extension.description "app"
//设置项目的android
setAppAndroidConfig extension.android
//设置项目的三方库依赖
setDependencies extension.dependencies
}
//设置library配置(只可以作为lib,不可单独调试)
setLibraryConfig = {
extension ->
//library,代表只是单纯的库,不需要依赖其他模块
extension.apply plugin: 'com.android.library'
extension.description "lib"
setLibAndroidConfig extension.android
setDependencies extension.dependencies
}
//设置动态切换组件,可单独调试的业务组件
setComponentConfig = {
extension ->
if (isModule.toBoolean()) {
extension.apply plugin: 'com.android.application'
extension.description "app"
} else {
extension.apply plugin: 'com.android.library'
extension.description "lib"
}
//设置通用Android配置
setComponentAndroidConfig extension.android
//设置通用依赖配置
setDependencies extension.dependencies
}
//设置application的android配置
setAppAndroidConfig = {
extension -> //extension 相当于 android 对象
extension.compileSdkVersion rootProject.ext.android.compileSdkVersion
extension.buildToolsVersion rootProject.ext.android.buildToolsVersion
extension.defaultConfig {
applicationId rootProject.ext.android.applicationId
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode rootProject.ext.android.versionCode
versionName rootProject.ext.android.versionName
multiDexEnabled true
multiDexKeepProguard file("${rootProject.rootDir}/multiDexKeep.pro") // keep specific classes using proguard syntax
multiDexKeepFile file("${rootProject.rootDir}/mainDexList.txt") // keep specific classes
extension.flavorDimensions "versionCode"
testInstrumentationRunner "android.support.tablet_test.runner.AndroidJUnitRunner"
//ARouter 编译生成路由
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
//使用的jdk版本
extension.compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
extension.buildFeatures {
dataBinding = true
viewBinding = true
}
}
//设置library公共的android配置
setLibAndroidConfig = {
extension -> //extension 相当于 android 对象
extension.compileSdkVersion rootProject.ext.android.compileSdkVersion
extension.buildToolsVersion rootProject.ext.android.buildToolsVersion
extension.defaultConfig {
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode rootProject.ext.android.versionCode
versionName rootProject.ext.android.versionName
testInstrumentationRunner "android.support.tablet_test.runner.AndroidJUnitRunner"
//ARouter 编译生成路由
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
extension.compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
extension.buildFeatures {
dataBinding = true
}
}
//设置业务组件的android配置
setComponentAndroidConfig = {
extension -> //extension 相当于 android 对象
extension.compileSdkVersion rootProject.ext.android.compileSdkVersion
extension.buildToolsVersion rootProject.ext.android.buildToolsVersion
extension.defaultConfig {
minSdkVersion rootProject.ext.android.minSdkVersion
targetSdkVersion rootProject.ext.android.targetSdkVersion
versionCode rootProject.ext.android.versionCode
versionName rootProject.ext.android.versionName
multiDexEnabled true
extension.flavorDimensions "versionCode"
multiDexKeepProguard file("${rootProject.rootDir}/multiDexKeep.pro") // keep specific classes using proguard syntax
multiDexKeepFile file("${rootProject.rootDir}/mainDexList.txt") // keep specific classes
testInstrumentationRunner "android.support.tablet_test.runner.AndroidJUnitRunner"
//ARouter 编译生成路由
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
extension.buildFeatures {
//启用自动绑定view id
dataBinding = true
viewBinding = true
}
//使用的jdk版本
extension.compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
//动态改变清单文件资源指向
extension.sourceSets {
main {
if (isModule.toBoolean()) {
manifest.srcFile 'src/main/module/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
}
//公用的三方库依赖,慎重引入,主要引入基础库依赖
setDependencies = {
extension ->
extension.implementation fileTree(dir: 'libs', include: ['*.jar'])
//ARouter 路由apt插件,用于生成相应代码,每个module都需要
extension.annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
extension.implementation 'com.alibaba:arouter-api:1.5.0'
extension.implementation 'com.alibaba:arouter-compiler:1.2.2'
//各组件公共的引用库,通常是android系统库,第三方库不建议添加此处
extension.implementation rootProject.ext.androidx.appcompat
extension.implementation rootProject.ext.androidx.material
extension.implementation rootProject.ext.androidx.constraintlayout
extension.implementation rootProject.ext.androidx.multidex
}
}
看着好像很复杂的样子,其实就是根据组件类型(application、library和可动态切换的组件),分成两组定义了plugin和androidConfig,除此之外还定义公共的dependencies。
解决资源文件冲突
组件化开发中,因为不同人员负责不同组件的开发,不可免的会出现资源文件重名问题,在最终整合编译时就可能出现资源文件冲突问题,为了解决这个问题可以在gradle文件中,使用resourcePrefix在编译时进行资源前缀检查,强制要求各组件资源文件按照固定格式命名,避免资源文件冲突问题。
//在各组件的build.gradle中
android {
//...
//资源前缀强制命名
resourcePrefix "signup_"
//或者直接用module命名限制
//resourcePrefix "${project.name}_"
//...
}
之后module中的所有资源文件在编译时都会进行检查,资源文件前缀是否是signup_,如果不是则会报错提示。
最终的项目结构如下:
总结
至此万里长征的第一步算是迈出去了,之后就可以步入正式开发阶段了。
参考博客: