前言:再说组件化之前,我们先来看一个app的界面:

这是电商平台苏宁易购app的首页。红色圈起来的部分,都是苏宁易购旗下的子业务,都是由不同的团队分别开发,毕竟业务不同,一个团队不可能开发这么多的业务。这就会有一个问题,各个团队之间如何协同作战呢?我们传统的做法是在项目中建立不同业务的文件夹(假设都是用原生进行开发),相关业务的代码都放在各自的文件夹内,然后各自团队开发各自的功能。但这样做有一个很大的弊端,就是每个业务都需要等其它的业务都开发完毕了才能打包进行测试,然而每个团队的进度由于业务的复杂性不同很难做到统一或者说差不多,提测的时间一再延长,这在快节奏的互联网公司是不被允许的。我们可以看下这个时候app的架构(假设有苏宁超市、苏宁生鲜、苏宁拼购、苏宁家电四个业务模块)

这是目前比较普遍的Android APP技术架构,各个业务模块糅合在一个工程中,相互依赖,只有简单的文件夹区分,没有真正意义上实现业务的模块化。而且由于各业务之间往往会相互调用,导致代码的高度耦合,维护成本大,不利于团队协同作战等等。随着业务越来越多,业务越来越复杂,我们就会碰到以下的问题:
- 实际业务变化非常快,但是单一工程的业务模块耦合度太高,牵一发而动全身。
- 对工程所做的任何修改都必须要编译整个工程。
- 功能测试和系统测试每次都要进行。
- 团队协同开发存在较多的冲突.不得不花费更多的时间去沟通和协调,并且在开发过程中,任何一位成员没办法专注于自己的功能点,影响开发效率。
- 不能灵活的对业务模块进行配置和组装。
各业务之间的关系就像下图一样

为了使各模块能灵活组装,同时方便协同开发,就必须对原有的app架构进行升级和改造。而新的app架构就是Android的组件化。
何为组件化?
- 每个业务都是一个单独的模块,都是既可以独立运行的app,又可以是以library的形式被主工程所依赖,我们称之为业务组件。
- 项目分为两种开发模式,集成模式和独立开发模式。集成模式下各业务组件以library的形式被主工程所依赖。独立开发模式下各业务组件作为单独的app,可自行打包运行。
- 每个模块之间的开发互相独立,每个模块之间通过路由(Router)进行联系。
- 整个工程有一个Main组件,指定app的入口,启动页面。和业务组件的原理一样,只不过由他来通过路由去管理各个业务组件,本身不具备任何业务代码。
- 整个工程有一个公共组件,这个组件被每个业务组件所依赖,提供多数业务组件需要的功能,例如提供网络请求功能、数据处理等基础功能。为所有的业务组件提供功能性服务(比如所有的工具类,甚至是资源文件),唯一不同的是它一直以library的形式被其它业务组件所依赖,本身不会以app的形式运行,仅仅提供服务。
对于他们之间的关系,我想用如下的图来形容

哎,画图真是个技术活儿,不会画好看的图,表达的也不够好,这里表格解释一下可能比较好。
名词 | 释义 |
---|---|
集成开发模式 | 所有的业务组件以library的形式被主工程所依赖,组成完整的app并打包运行。 |
独立开发模式 | 所有的业务组件以独立app的形式存在,可自行打包并运行。 |
主工程 | 负责管理各个业务组件和打包apk,本身不存在任何的业务代码。 |
业务组件 | 由具体的业务独立出来的业一个工程 。 |
Main组件 | 是业务组件的一种,指定app的启动页、主页面等。 |
Commom组件 | 单独的一个组件,只会以library的形式存在,并且被每个具体的业务组件所依赖,提供基础服务。 |
好吧,光说不练假把式,来看一看组件化实现的具体流程。通过一个例子可能更容易理解。
新建一个工程ModulesDemo

很简单,就是普通的一个Android项目的目录结构。app是一个Module,这里把app作为主工程。和平时开发有两点不同:
- 因为业务都被业务组件拆分了,所以主工程中不需要任何Activity和资源文件。
- 主工程一般需要一个自定义的Application,并在AndroidManifest.xml文件中指定该Application,这个类应该建立在主工程中。
- 主工程中没有任何Activity,所以AndroidManifest.xml中启动页应该是来自Main组件中的启动页,如SplashActivity。
新建Main组件
就是在工程中新建module即可,这里的module就相当于组件。命名为module_main:

主工程中的build.gradle代码:
apply plugin: 'com.android.application'
android {
compileSdkVersion 28
defaultConfig {
applicationId "pack.suning.com.modulesdemo"
minSdkVersion 26
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//依赖Main组件。
implementation project(':module_main')
}
主工程中的AndroidManifest.xml代码:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.suning.demo">
<application
//指定Application
android:name=".MyApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true">
//指定app入口为main组件中的SplashActivity
<activity android:name="pack.suning.com.main.SplashActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
需要注意的一点,由于拆分出了许多业务组件,所以各自组件中资源文件如图片的名称尽量以module的名称为前缀,避免冲突。先新建一个Common组件(右键工程->新建module>命名为module_commom):
Common组件的build.gradle代码:
//Common组件永远以library形式存在。
apply plugin: 'com.android.library'
android {
compileSdkVersion 28
defaultConfig {
minSdkVersion 26
targetSdkVersion 28
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:appcompat-v7:28.0.0-alpha1'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'com.android.support.test:runner:1.0.2'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
}
可以看到compileSdkVersion、minSdkVersion、targetSdkVersion、依赖库版本号等信息,每个组件都是写死的,不便于统一管理,可以在工程根目录下的build.gradle文件中定义这些常量,每个组件中引用常量,方便统一管理:
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath'com.android.tools.build:gradle:3.2.1'
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
ext {
//App sdk version
compileSdkVersion = 28
minSdkVersion = 19
targetSdkVersion = 28
// App dependencies version
retrofitVersion = "2.3.0"
rxjavaVersion = "2.0.6"
}
在Common组件的的build.gradle组件中引用:
//Common组件永远以library形式存在。
apply plugin: 'com.android.library'
android {
compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig {
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode rootProject.ext.versionCode
versionName rootProject.ext.versionName
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
testImplementation "junit:junit:$rootProject.ext.junitVersion"
androidTestImplementation "com.android.support.test:runner:$rootProject.ext.testRunnerVersion"
androidTestImplementation "com.android.support.test.espresso:espresso-core:$rootProject.ext.testEspressoVersion"
api "com.android.support:appcompat-v7:$rootProject.ext.supportAppcompatV7Version"
//这里要用api或compile关键字,否则别的组件无法使用这里引用的依赖库,切记切记。
api "com.squareup.retrofit2:retrofit:$rootProject.ext.retrofitVersion"
api "io.reactivex.rxjava2:rxjava:$rootProject.ext.rxjavaVersion"
}
其他的组件和主工程同理,凡是有定义compileSdkVersion、minSdkVersion、targetSdkVersion、依赖库版本的组件都可以替换成全局写法便于管理,这里就不贴代码了。
AndroidManifest文件的合并
我们知道,无论是组件还是主工程,都会存在一个AndroidManifest文件。当把所有的组件都合并成一个app的时候,就需要把AndroidManifest文件也合并,因为各个组件的AndroidManifest都会存在如下代码来标记入口Activity:
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
而我们知道,AndroidManifest中不能设置多个入口Activity。所以当所有组件合并到一起的时候,就必须对AndroidManifest文件进行合并。
合并策略:
- Common组件由于没有任何界面,所以无需指定任何入口Activity,其AndroidManifest文件代码如下:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="pack.suning.com.common" />
- 各业务组件的合并。由于各个业务组件在独立开发时要能够独立打包运行,所以在独立开发时需要指定Lunch Activity;合并到主工程时,Lunch Activity在整个工程中只能存在一个。看来好像无法解决,一个AndroidManifest没法解决这个问题。咋办呢?用两套AndroidManifest文件,一套是组件作为依赖合并到主工程时使用,一套是组件独立开发时使用。看下具体的实现流程:
新建一个苏宁超市组件(新建方式同上)。
指定独立开发时的AndroidManifest:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="pack.suning.com.module_market" >
<application>
<activity android:name="cn.suning.market.activity.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
独立开发时需要指定Lunch Activity
指定合并时的AndroidManifest:
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="pack.suning.com.module_market" >
<application>
<activity android:name="cn.suning.market.activity.MainActivity"/>
</application>
</manifest>
合并时,只要把Activity注册到清单文件中即可,不需要指定其为Lunch Activity,因为合并后Lunch Activity是Main组件中的MainActivity。
这两套AndroidManifest文件如何切换?我们要的效果是单独开发时指定第一套,合并到整个工程师时使用第二套。首先在项目的根目录的gradle.properties文件新加一个变量:
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
together=true
together表示是否合并。
在market组件中的build.gradle文件中加入如下代码:
sourceSets {
main {
if (together.toBoolean()) {
//指定合并模式下AndroidManifest文件目录。
manifest.srcFile 'src/main/together/AndroidManifest.xml'
//合并模式下排除debug文件夹中的所有Java文件,排除可能不必要的问题。
java {
exclude 'debug/**'
}
} else {
//指定独立开发时AndroidManifest文件目录
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
两种模式下的清单文件上文都已经给出示例,只需要新建main目录下新建一个together文件夹,并把合并模式下的清单文件拷贝进去即可。
注意:组件中需要访问资源都可以common组件中获取,也可以从组件自身取,如何取的标准是这个资源是不是所有业务组件都通用的。 比如指定AppTheme,如果所有的组件的主题都一样,就可以在Common中定义主题,然后在其它组件中引用,只需要该组件引用了Common组件即可。
<application
android:theme="@style/AppTheme"/>
build.gradle文件的处理:
还有一个问题,就是业务组件我们新建的时候都是新建module的,而module是不能作为app单独运行的。如果要作为app运行,需要修改build.gradle中的代码:
把
apply plugin: 'com.android.library'
修改为:
apply plugin: 'com.android.application'
即可,但是这里需要动态变化,每次手动去改所有的组件不太优雅,这就又用到了我们上面定义的常量together,根据这个常量来判断是独立还是合并:
if (together.toBoolean()) {
//合并模式下以library的形式被Main组件所依赖
apply plugin: 'com.android.library'
} else {
//独立模式下可以作为app单独运行。
apply plugin: 'com.android.application'
}
还有Main组件中build.gradle文件做稍许修改:
api project(':module_common')
if (together.toBoolean()) {
//合并模式下依赖所有的业务组件。
api project(':module_market')
}
到这里差不多已经实现了Android项目的组件化,我们把together的值改为false,然后编译整个项目。可以看到项目的结构如下:

然后看下可以运行的项目:

可以看到独立模式下,所有的业务组件都可以作为一个单独的app运行。
再看下合并模式下编译项目后:

可以看到,合并后,各个组件已经不能单独作为app运行了。只需要改个变量的值,就可以对组件的性质做一个切换,且可以实现主工程对各个业务组件的热插拔,灵活接入和移除。
组件化的混淆方案
组件化项目的Java代码混淆方案采用在合并模式下集中在主工程中混淆,各个业务组件不配置混淆文件。合并模式模式下在主工程中build.gradle文件的release构建类型中开启混淆属性,其他buildTypes配置方案跟普通项目保持一致,Java混淆配置文件也放置在主工程中,各个业务组件的混淆配置规则都应该在主工程中的混淆配置文件中添加和修改。
组件化页面的跳转方案
通常页面的跳转是通过原生的路由方案由显示和隐式Intent来进行页面的跳转。组单一工程的时候用原生的路由方案足以进行页面的管理,但是当项目组件化之后,每个组件可能包含相同的页面名称,这时的页面跳转会存在直接的类依赖的问题,导致耦合非常严重,这里我们采用阿里的ARouter路由框架来实现各个组件之间页面的跳转。用法如下:
- 导入依赖
由于每个组件中都会用到页面跳转,所以在common组件中引入ARouter框架即可,在common组件的build.gradle中添加如下代码:
api'com.alibaba:arouter-api:1.3.1'
然后在每个用到ARouter的业务组件中的build.gradle中添加如下代码:

代码如下:
javaCompileOptions {
annotationProcessorOptions {
arguments = [moduleName :project.getName() ]
} }
annotationProcessor'com.alibaba:arouter-compiler:1.1.4'
注意,这里是每个业务组件都需要配置这些代码,否则框架会提示找不到该页面的路径。
- 在Application中初始化
public class MyApplication extends Application {
public final String TAG = this.getPackageName();
private boolean ARouterDebuger = true;
@Override
public void onCreate() {
super.onCreate();
Log.e(TAG, "app init...");
if(ARouterDebuger){
ARouter.openLog();//打印日志
ARouter.openDebug();//开启调试模式,上线时需要关闭(在InstantRun模式下运行则必须开启)
}
//初始化需放在上面两行代码后面。
ARouter.init(this);
}
}
- 开始使用
在Activity或Frament中用@Route注解标记页面的路径,在onCreate中进行注入。举个例子,在main组件中的SplashActivity中增加一个按钮,点击跳转到market组件的MainActivity中,则market组件的MainActivity代码如下:
@Route(path = "/market/MainActivity")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ARouter.getInstance().inject(this);
}
}
main组件中的SplashActivity同样需要添加注解并注入,代码如下:
@Route(path = "/main/SplashActivity")
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
ARouter.getInstance().inject(this);
findViewById(R.id.main_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ARouter.getInstance().build("/market/MainActivity").navigation();
}
});
}
}
若是单纯的页面跳转,通过ARouter.getInstance().build(“页面路径”).navigation()一句代码即可实现跳转。需要注意一点:
页面的路径至少需要有两级,/xx/xx。
要想携带数据,则代码如下:
@Route(path = "/main/SplashActivity")
public class SplashActivity extends AppCompatActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_splash);
ARouter.getInstance().inject(this);
findViewById(R.id.main_button).setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
ARouter.getInstance()
.build("/market/MainActivity")
.withString("name","张三")
.navigation();
}
});
}
}
ARouter支持所有所有常用类型的数据,包括可序列化的对象。
接收数据:
在目标页面用 @Autowired标记一个变量用来接收传递过来的数据,这里传递一个String类型的数据,则MainActivity的代码如下:
@Route(path = "/market/MainActivity")
public class MainActivity extends AppCompatActivity {
@Autowired(name = "name")
String name;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_market_main);
ARouter.getInstance().inject(this);
Log.e("TAG--------------NAME",name);
}
}
看下运行结果:

@Autowired注解的name属性是传递过来数据的key,变量name用来接收数据。
好了,ARouter只介绍到这里,具体更加深入的用法还请大家自行百度,本篇的重点不在这里,在此不再赘述。
总结:组件化是一种思想,是对于整个项目不同业务逻辑的拆分,使各个业务组件之间既不会相互依赖,又可以使整个项目对各个业务组件实现热插拔,需要时接入,不需要时移除。很好的实现了整个项目的灵活配置,本篇中关于组件化的实现,若是有不足或者缺陷,还请各位积极留言探讨~
所有代码已经上传至GitHub,代码地址:https://github.com/BestAndroider/ModulesDemo 。