1.概述
早期用 Eclipse 进行 Android 开发,创建一个工程,对于引入开源框架时候,采用的是用库的形式进行引入;到后来有 Android Studio 的出现,在 Project 下可以存在多个 module,除了要运行的 module 是 application 外,其他 module 都是 library。在每个 module 的 build.gradle 文件中区分:
应用:
apply plugin: 'com.android.application'
库:
apply plugin: 'com.android.library'
2. 模块化和组件化
2.1 模块化
把常用的功能、控件、基础类、第三方库、权限等公共部分抽离封装,把业务拆分成N个模块进行独立(module)的管理;而所有的业务组件都依赖于封装的基础库,业务组件之间不做依赖,这样的目的是为了让每个业务模块能单独运行。模块化的特点是:模块之间解耦,可以独立管理。
如下项目采用模块化,只有一个模块是 application,其他的都是 libary:
优缺点:
如上图所示,将自定义的软键盘、自定义控件等分为不同的模块,彼此之间降低耦合;而且模块可以复用,甚至不同的项目都可以复用。随着业务越来越多,模块也越来越多,当模块原来越多,每次进行编译都要长时间的等待。
2.2 组件化
将一个app的代码拆分成几份独立的组件,组件之间是低耦合的,可以独立编译打包;也可以将组件打包到一个apk中。组件化的核心是角色的转换。 在打包时, 是library; 在调试时, 是application。组件化后的优点:每个模块可独立编译,提高了编译速度;开发只负责自己的模块,还可以再做的隔离一些,每个业务线只可见自己业务模块的代码,避免了误修改和版本管理问题;公共的Lib依然是个独立模块。
组件化与模块化的区别:是每个模块的角色的转换,一个组件可以独立编译打包,也可以作为lib集成到整个apk中。
3.组件化的实践
最近瑞幸咖啡正处于风口浪尖,就以瑞幸咖啡的APP为模板:
分为五个模块:首页、菜单、潮品、购物车、我的。
3.1 创建模块,并实现组件模式和集成模式的切换
创建五个module,但是五个 module 都是application,可以独立运行,这里对 application 模块名进行统一格式进行命名。对于application 模块以 module_X 的格式进行命名,而对于 library 模块以 lib_X 的格式进行命名。
创建了三个不同的module,如何做到在调试阶段,每个module都能独立运行,在发布时候集成编译成一个APP呢?Android Studio中的Module主要有两种属性,分别为:application属性和library属性;在项目的根目录下的文件 gradle.properties 中定义相关常量,所有的 module 都能读取该常量。用这种办法来切换module为应用还是库。
gradle.properties 文件中定义五个参数用来控制五个不同的模块是否是独立编译:
# isBuildModuleXX 用于控制是否单独编译,true:单独编译,false:library形式
#首页模块
isBuildModuleHome=true
#菜单模块
isBuildModuleMenu=true
#潮品模块
isBuildModuleFashions=true
#购物车模块
isBuildModuleShopping=true
#我的模块
isBuildModuleUser=true
在五个 module 的 build.gradle 中读取 gradle.properties中的常量,决定 module 为应用还是库:
if (isBuildModuleHome.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
3.2 不同的模块,确保 SDK 和引入库版本号一致
不同模块的依赖 SDK 版本不一致或者引入的库版本不一致会导致编译问题和兼容性问题。解决办法是在主项目最外层用一个文件来统一管理引入库和 SDK 的版本。
在工程的根目录创建文件 dependency.gradle:
ext {
minSdkVersion = 16
targetSdkVersion = 29
compileSdkVersion = 29
buildToolsVersion = "29.0.3"
versionCode = 1
versionName = "1.0"
versions = [
...
...
]
dependencies = [
...
...
]
}
在工程的 build.gradle 中引入文件 dependency.gradle:
apply from: "dependency.gradle"
在模块中的 build.gradle 实现对文件 dependency.gradle 的使用:
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
3.3 AndroidManifest 管理问题
在创建模块的时候,是以 application 的形式进行创建的,所以每个模块都有程序的启动入口。但是一旦整合起来后,如果继续保持多个启动入口,明显是有问题的。
在 main 目录下新增文件夹 compound,用于模块以 library 形式存在时候提供 AndroidManifest.xml ;而Android studio 产生的原路径下的 AndroidManifest.xml 文件保留,用于存储单独编译时候的 AndroidManifest.xml 文件。
在 library 状态下的 AndroidManifest.xml 文件:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="zzw.cn.component.home">
<application
android:theme="@style/AppTheme">
<activity android:name=".HomeActivity">
</activity>
</application>
</manifest>
单独编译状态下的 AndroidManifest.xml 文件:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="zzw.cn.component.home">
<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/AppTheme">
<activity android:name=".HomeActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
不同状态下定义好了不同的 AndroidManifest.xml 文件了,而 AndroidStudio 该如何识别呢?
在组件的 build.gradle 文件的 android 中进行 manifest 的管理:
android {
...
...
sourceSets {
main {
if (isBuildModuleHome.toBoolean()) {
manifest.srcFile 'src/main/AndroidManifest.xml'
}else{
manifest.srcFile 'src/main/compound/AndroidManifest.xml'
}
}
}
}
3.4 基础公共组件
以 library 形式创建模块 lib_base 和 lib_common,其中 lib_base用于第三方库的统一依赖,lib_common 用于封装公共部分,如对于网络请求等的二次封装等。
lib_base 的 build.gradle文件如下所示:
apply plugin: 'com.android.library'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
def root_dependencies = rootProject.ext.dependencies
android {
compileSdkVersion rootProject.ext.compileSdkVersion
buildToolsVersion rootProject.ext.buildToolsVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles 'consumer-rules.pro'
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api root_dependencies.kotlin
api root_dependencies.appcompat
api root_dependencies.androidx_core
api root_dependencies.glide
annotationProcessor root_dependencies.glide_compiler
api root_dependencies.retrofit
api root_dependencies.converter_gson
api root_dependencies.retrofit_adapter_rxjava
api root_dependencies.rxjava
api root_dependencies.constraintlayout
api root_dependencies.okhttp
api root_dependencies.eventbus
api root_dependencies.easypermissions
api root_dependencies.coroutines_core
}
其中对于依赖的第三方库版本的定义在工程根目录下的文件 dependency.gradle 中:
ext {
minSdkVersion = 16
targetSdkVersion = 29
compileSdkVersion = 29
buildToolsVersion = "29.0.3"
versionCode = 1
versionName = "1.0"
versions = [
appcompat : "1.1.0",
androidx_core : "1.2.0",
kotlin : "1.3.70",
glide : "4.11.0",
glide_compiler : "4.11.0",
retrofit : "2.7.2",
converter_gson : "2.7.2",
retrofit_adapter_rxjava: "2.7.2",
rxjava : "3.0.0",
constraintlayout : "1.1.3",
okhttp : "4.0.0",
eventbus : "3.2.0",
easypermissions : "3.0.0",
coroutines_core : "1.3.5"
]
dependencies = [
kotlin : "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$versions.kotlin",
androidx_core : "androidx.core:core-ktx:$versions.androidx_core",
appcompat : "androidx.appcompat:appcompat:$versions.appcompat",
glide : "com.github.bumptech.glide:glide:$versions.glide",
glide_compiler : "com.github.bumptech.glide:compiler:$versions.glide_compiler",
retrofit : "com.squareup.retrofit2:retrofit:$versions.retrofit",
converter_gson : "com.squareup.retrofit2:converter-gson:$versions.converter_gson",
retrofit_adapter_rxjava: "com.squareup.retrofit2:adapter-rxjava2:$versions.retrofit_adapter_rxjava",
rxjava : "io.reactivex.rxjava3:rxjava:$versions.rxjava",
constraintlayout : "androidx.constraintlayout:constraintlayout:$versions.constraintlayout",
okhttp : "com.squareup.okhttp3:okhttp:$versions.okhttp",
eventbus : "org.greenrobot:eventbus:$versions.eventbus",
easypermissions : "pub.devrel:easypermissions:$versions.easypermissions",
coroutines_core : "org.jetbrains.kotlinx:kotlinx-coroutines-core:$versions.coroutines_core"
]
}
在 lib_commom 中定义对 lib_base的依赖,build.gradle 文件如下:
...
...
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api project(path: ':lib_base')
}
业务模块实现对 lib_commom 的依赖:
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project(':lib_common')
}
3.5 使用 ARouter 进行组件之间的调用和通信
由于业务组件间不存在依赖关系,不可以通过 Intent 进行跳转。组件化的目的是为了解决模块间的强依赖问题,这时候就需要借助于路由的,我使用的是阿里巴巴的开源框架 ARouter。
ARouter 的地址:https://github.com/alibaba/ARouter
3.5.1 ARouter 的导入
根据官方的文档,需要添加的依赖有:
dependencies {
// Replace with the latest version
compile 'com.alibaba:arouter-api:?'
annotationProcessor 'com.alibaba:arouter-compiler:?'
...
}
在这里,我将 arouter-api 添加在 lib_base 中,arouter-compiler 必须添加在每个组件模块中。由于此项目采用的语言为 kotlin,配置如下:
需要在每个组件模块中添加:
android {
...
...
defaultConfig {
...
...
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}
}
...
...
}
dependencies {
...
...
kapt root_dependencies.arouter_compiler
...
...
}
而且还需要添加:
apply plugin: 'kotlin-kapt'
3.5.2 页面间的简单跳转
在目标页面 Activity/Fragment 类上写上 Route Path 的注解,这里的路径需要注意的是至少需要有两级,/xx/xx。
@Route(path = "/home/HomeActivity")
class HomeActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_home)
}
override fun onDestroy() {
super.onDestroy()
}
}
在需要进行跳转的页面进行触发跳转:
fun click(v: View) {
when (v.id) {
R.id.btn_home -> {
ARouter.getInstance().build("/home/HomeActivity").navigation()
}
R.id.btn_menu -> {
ARouter.getInstance().build("/menu/MenuActivity").navigation()
}
R.id.btn_fashions -> {
ARouter.getInstance().build("/fashions/FashionsActivity").navigation()
}
R.id.btn_shopping -> {
ARouter.getInstance().build("/shopping/ShoppingActivity").navigation()
}
R.id.btn_user -> {
ARouter.getInstance().build("/user/UserActivity").navigation()
}
}
}
效果如下:
这里只是列举了页面的简单跳转,如果需要在跳转的时候携带参数等,可以参照 ARouter 的介绍:https://github.com/alibaba/ARouter
4.总结
此篇文章只是针对组件化进行简单的实现,其中还有很多问题没有描述具体的解决办法,比如如何在每个模块获取登录信息、如何避免资源名字冲突等问题。