安卓组件化开发是老生常谈的问题,基础的模块化开发教程很多,本系列教程展现从零开始,到整个系统搭建的过程,设计项目组件化结构、MVP设计模式、组件间通信路由框架、WebSocket网络交互基础库的设计、推送服务基础库的设计、UI统一风格基础库的设计、数据库交互基础库的设计、以及业务相关的实际应用场景问题。

系列文章

Android组件化-基础框架搭建

Android组件化-组件间通信BRouter

Android组件化-风格统一&主题变色

Android组件化-MVP设计模式

一、模块化开发的几个问题

  • 资源冲突
  • 单模块调试
  • 组件间通信

项目由一个空壳模块app、一个主界面app-main模块、若干app-xxx业务模块组成、一个公共库lib-common、若干基础库组成,借本结构如下

使用Android Studio作为IDE开发,项目模块基本文件结构如下:

二、项目搭建

项目搭建主要分三步,搭建基础库模块lib-xxx、全局公共库模块lib-common,搭建业务应用模块app-xxx,整合业务模块到app空壳模块。

2.1 基础库

新建一个模块作为lib,在AndroidStudio中 File->New->New Module…->Android Library,LibraryName为db,ModuleName为lib-db,如图:

lib-db模块中build.gradle修改如下,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 用来定义此模块是lib库模块还是应用模块
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'])

implementation 'com.android.support:appcompat-v7:26.1.0'
testImplementation 'junit:junit:4.12'
}

此时lib-db 未引入任何第三方库,rootProject.ext.compileSdkVersion为项目根目录build.gradle中定义的统一配置,方便做全局修改。根目录build.gradle如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
ext {
// SDK Tool Version
minSdkVersion = 21
targetSdkVersion = 26
compileSdkVersion = 26
buildToolsVersion = '25.0.3'

// Build Version
versionCode = 1
versionName = "1.0"

// java version
javaVersion = JavaVersion.VERSION_1_8

// 第三方依赖的版本
supportVersion = "26.1.0"
butterknifeVersion = "8.8.1"
gsonVersion = "2.8.5"
rxJavaVersion = "2.2.2"
rxAndroidVersion = "2.1.0"
rxBindingVersion = "1.0.0"
rxPermissionVersion = "0.10.2"
retrofitVersion = "2.4.0"
okHttpVersion = "3.11.0"
eventBusVersion = "3.1.1"
greenDaoVersion = "3.2.2"
lottieVersion = "2.6.1"
}

buildscript {

repositories {
google()
jcenter()
}

dependencies {
classpath 'com.android.tools.build:gradle:3.0.1'
}
}

// 后期框架引入诸多第三方库,需要添加更多远程仓库
allprojects {
repositories {
google()
jcenter()
mavenCentral()
maven { url 'https://jitpack.io' }
}
}

task clean(type: Delete) {
delete rootProject.buildDir
}

// 统一配置android support version
subprojects {
project.configurations.all {
resolutionStrategy.eachDependency { details ->
if (details.requested.group == 'com.android.support'
&& !details.requested.name.contains('multidex')) {
details.useVersion "26.1.0"
}
}
}
}

lib库模块中引入第三方库需注意,假如此库中 方法/View控件 需要在应用模块中使用,则此依赖需要使用api引入,如

1
api "com.thirdpart.app:app:1.0"

类似地新建其他几个lib库模块lib-log、lib-push、lib-apptool、lib-socket。

2.2 common库

lib-common和基础库同样是lib库模块,搭建的方法相同,不同点在于lib-common模块通过 ‘compile project’ 依赖其它所有的基础库模块,并直接被其它应用模块所依赖,实现向应用模块提供服务。

新建lib-common后,build.gradle配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
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 {
debug {

}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])

// Support Libary
compile "com.android.support:design:$rootProject.supportVersion"
compile "com.android.support:cardview-v7:$rootProject.supportVersion"
compile "com.android.support:appcompat-v7:$rootProject.supportVersion"
compile "com.android.support:recyclerview-v7:$rootProject.supportVersion"
compile "com.android.support.constraint:constraint-layout:1.1.3"

// Test
testImplementation 'junit:junit:4.12'

// 其它基础库模块
compile project(':lib-ui')
compile project(':lib-log')
compile project(':lib-push')
compile project(':lib-router')
compile project(':lib-socket')
compile project(':lib-apptool')

api "com.google.code.gson:gson:$rootProject.gsonVersion"

// Rx
api "io.reactivex.rxjava2:rxjava:$rootProject.rxJavaVersion"
api "io.reactivex.rxjava2:rxandroid:$rootProject.rxAndroidVersion"
// RxBinding
api "com.jakewharton.rxbinding:rxbinding:$rootProject.rxBindingVersion"
api "com.jakewharton.rxbinding:rxbinding-appcompat-v7:$rootProject.rxBindingVersion"
api "com.jakewharton.rxbinding:rxbinding-recyclerview-v7:$rootProject.rxBindingVersion"
api "com.jakewharton.rxbinding:rxbinding-design:$rootProject.rxBindingVersion"

// RxPermission
api "com.github.tbruyelle:rxpermissions:$rootProject.rxPermissionVersion"
// network
api "com.squareup.okhttp3:okhttp:$rootProject.okHttpVersion"
api "com.squareup.okhttp3:logging-interceptor:$rootProject.okHttpVersion"
api "com.squareup.retrofit2:retrofit:$rootProject.retrofitVersion"
api "com.squareup.retrofit2:converter-gson:$rootProject.retrofitVersion"
api "com.squareup.retrofit2:adapter-rxjava:$rootProject.retrofitVersion"
// EventBus
api "org.greenrobot:eventbus:$rootProject.eventBusVersion"

// RefreshLayout
api 'com.scwang.smartrefresh:SmartRefreshLayout:1.1.0-alpha-14'
api 'com.scwang.smartrefresh:SmartRefreshHeader:1.0.3'//没有使用特殊Header,可以不加这行

// Views
api("com.airbnb.android:lottie:$rootProject.lottieVersion") {
exclude group: 'com.android.support'
}
}

2.3 业务模块

业务模块如app-mine(个人中心)、app-message(消息中心)等既可作为应用模块独立运行,也可作为库模块被空壳app依赖,新建业务模块 File->New Module->Phone & Tablet Module,

app-message模块中build.gradle配置如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// 模块切换至应用模块或库模块
if (moduling.toBoolean()) {
apply plugin: 'com.android.application'
} else {
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'
}
}

// 为避免资源冲突,为不同模块的资源加上前缀,资源文件前缀手动加入
resourcePrefix 'message_'

// 库模块和应用模块使用不同的Manifest.xml和Application
sourceSets {
main {
if (moduling.toBoolean()) {
// 注意,粘贴代码时mainfest容易变成Manifest
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
println '[Module-Message]: Appling Application...'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
java {
exclude 'debug/**'
}
println '[Module-Message]: Appling Library...'
}
}
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation 'com.android.support:support-v4:26.1.0'

// 依赖 common 库模块
compile project(':lib-common')
}

如上的gradle配置涉及到单组件运行调试、组件间资源文件冲突。

粘贴代码时 manifest.srcFile 容易变成 Manifest.srcFile

2.4 空壳app模块

空壳app是作为应用模块的“组合车间”,通过依赖不同的/所有的应用模块,将各个功能模块的功能聚合起来,实现app应有的功能,app模块中包含:

  • build.gradle
  • app的名称、图标
  • 全局的Application文件

build.gradle如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
apply plugin: 'com.android.application'

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"

// jni配置
ndk {
abiFilters 'armeabi', 'armeabi-v7a', 'arm64-v8a'
}
}

buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
// 资源文件前缀
resourcePrefix 'app_'
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])

if (moduling.toBoolean()) {
compile project(':lib-common')
} else {
// 依赖需要的应用模块
compile project(':app-main')
compile project(':app-mine')
compile project(':app-message')
}
}

app名称、图标配置在空壳中方便全局修改;

app还包含Application文件,在此做一些初始化操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AppApplication extends CommonApplication {

@Override
public void onCreate() {
super.onCreate();

BLog.d("[App]: Application Starting...");

initRouter();

if (shouldInit()) {
PushClient.getInstance().init(this)..setListener(new PushHandler());
}
}
}

除此之外,空壳app不包含任何逻辑、业务代码,登录注册页、欢迎页、主界面包含在app-main中,也是作为应用模块被app依赖。app模块是真正意义上的空壳。

三、单模块调试

3.1 模块动态切换

app-message模块里builld.gradle开头的moduling配置在gradle.properties中,为boolean类型,

1
2
# debug settings
moduling=false

通过配置moduling的值,实现app-message作为独立应用模块或库模块的切换。

当build.gradle的apply plugin配置为’com.android.application’时,app-message作为应用模块:

1
apply plugin: 'com.android.application'

此时模块图标为

当build.gradle的apply plugin配置为’com.android.library’时,app-message作为库模块:

1
apply plugin: 'com.android.library'

此时模块图标为

3.2 使用不同的Manifest和Application

在应用模块的build.gradle中有如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
...

android {
...

// 库模块和应用模块使用不同的 Manifest.xml 和 Application
sourceSets {
main {
if (moduling.toBoolean()) {
// 注意,粘贴代码时mainfest容易变成Manifest
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
println '[Module-Message]: Appling Application...'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
java {
exclude 'debug/**'
}
println '[Module-Message]: Appling Library...'
}
}
}
...
}

当模块为独立应用模块时,模块使用 ‘src/main/debug/‘下的AndroidManifest.xml,当模块作为库模块被app空壳依赖时,使用’src/main’下的AndroidManifest.xml。

‘src/main/debug/AndroidManifest.xml’配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.blackist.modulize.message">

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/message_app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/message_AppTheme">
<activity android:name=".view.MessageActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest>

单模块调试时需要有独立的启动Activity、图标、app名称,如果需要独立的Application进行初始化操作,也在此配置。

‘src/main/AndroidManifest.xml’配置如下:

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.blackist.modulize.message">

<application>
<activity android:name=".view.MessageActivity" />
</application>

</manifest>

作为库模块被依赖时,只需要在Manifest中注册activity、service等即可。

单模块开发、调试的时候,activity的配置只注册在debug下的Manifest,请记得手动拷贝至main下Manifest中。

四、资源冲突合并问题

不同模块中资源文件难免会有相似的资源文件,比如app-message有个list_icon的drawable、app-main中也有个list_icon的drawable,非单模块调试的时候需要合并资源文件,聚合在app空壳中,此时会引起资源冲突。

一种比较好的实践就是在各模块中添加资源前缀,尽量避免冲突,这要求项目组成员遵守规范,即资源命名为message_list_icon和main_list_icon。

xml中资源可以被android studio检查,如string.xml或color.xml中的资源,但图片类的资源不会被检查,需要开发者严格遵守约定。

组件化开发还有个问题就是,在各应用模块中使用if-else判断资源id,而不是switch,

1
2
3
4
5
if(R.id.message_list_icon == id) {
...
} else if (R.id.message_list_text == id) {
...
}

五、组件间通信

因为应用模块(app-xxx)依赖库模块(lib-xxx),所以应用模块可以直接使用库模块中的View控件和方法,而应用模块之间项目隔离,无法直接使用其它模块的控件和方法,需要使用组件间通信工具来协助完成。

使用广播、EventBus会有一些相应的问题,比较好的一种实践就是阿里巴巴开源的ARouter以及一些自定义的组件间通信框架如Brouter,组件间通信在组件化开发中经常使用。

5.1 app-main主界面的实现

主界面有三部分主页、消息中心、个人中心,主页一般在核心的业务逻辑模块中,消息中心页在app-message模块中,个人中心页在app-mine模块中。

app-main中MainActivity负责处理三个页面的展示和切换,即一个Activity包含三个Fragment,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
...
private BaseFragment currentFragment;
private BaseFragment mainFragment;
private BaseFragment mineFragment;
private BaseFragment messageFragment;

...

BRouterRes roomRes = BRouter.push(
getApplicationContext(),
BRouterReq.build().action("room").path("room/home")
);
mainFragment = (BaseFragment) roomRes.data();

BRouterRes mineRes = BRouter.push(
getApplicationContext(),
BRouterReq.build().action("mine").path("/mine/info")
);
mineFragment = (BaseFragment) mineRes.data();

BRouterRes messageRes = BRouter.push(
getApplicationContext(),
BRouterReq.build().action("message").path("/message/info")
);
messageFragment = (BaseFragment) messageRes.data();

// fragment 展示
...

5.2 跨模块数据访问

直接返回数据

类似于获取其它模块的Fragment,

1
2
3
4
5
BRouterRes roomRes = BRouter.push(
getApplicationContext(),
BRouterReq.build().action("room").path("room/home")
);
mainFragment = (BaseFragment) roomRes.data();

startActivityForResult

1
2
3
4
5
6
7
8
9
10
11
12
13
BRouterRes res = BRouter.push(
getContext(),
BRouterReq.build().action("repair").path("repair/main/clazz"));
Intent intent = new Intent(getActivity(), (Class) res.data());
intent.putExtra("COMMAD", "func");
getActivity().startActivityForResult(intent, REQUEST_CODE);


@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
...
}

组件化开发基本架构到此已有雏形,可以进行简单的业务功能开发。

项目Github地址:https://github.com/blackist/modulize

(完)