0%

02-组件化-第一节

一、起因

早期的时候,一个项目都在一个一起,通过包名来控制不同的模块和功能。这种方式的缺点:

  • 层次混乱:无论怎么做分包,随着项目增大,就会失去层次感,接手的人扑街
  • 耦合度高,低内聚:。包名约束太弱,稍不注意就不同业务包直接相互调用
  • 不易于版本管理,容易代码冲突
  • 难以重用

所以,很容易想到组件化的好处:

  • 不相互依赖
  • 可以互相交互
  • 高度解耦
  • 自由拆卸组合
  • 重复利用

二、组件化环境

各个模块组件都能单独打包,对于测试是很友好的。在正式上线的时候,所有模块都需要App 壳才能运行。

2.1 Gradle

gradle 的根在哪里? 就是 settings.gradle 这个文件!然后,我们整个项目有个 gradle ,这个 project 就在项目根目录下 build.gradle 。 build 的步骤就是:

  1. settings.gradle

  2. Project 级别的 build.gradle

  3. 壳工程的 build.gradle

  4. library 中的 build.gradle

app 和 各个 module 中都有 gradle 文件,里面可能会包含相同代码,比如 编译工具版本、最小支持版本。我们可以在project 下面新建 gradle ,比如命名为 derry.gradle,在里面写上 ext 扩展块,代码如下:

1
2
3
4
5
6
ext {
compileSdkVersion 30
defaultConfit {
minSdkVersion 16
}
}

不过此时还不能被系统所认识,只能知道这是 key-value 的形式,我们只能将其引入到project 所属的gradle 中才能实现,那么,在 Project 的 build.gradle 中可以写如下代码:

1
apply from: 'derry.gradle'

所以,在各个模块中,可以使用这个公共的gradle 文件了:

1
2
3
4
def ext = rootProject.ext
android {
compileSdkVersion ext.compileSdkVersion
}

这里不去定义 def ext 也是可以的,但是为什么要这么做呢?这是为了性能考虑,因为这样定义一下就相当于局部变量了,能提高运行速度(老师说这个可以在面试时候去说的,说明真的玩过gradle)。

最后,放上App 壳和各个模块之间的配置,可以做到:

  • 正式环境和测试环境的部署

  • 当测试环境时,各个模块可以单独运行和打包,正式环境的时候必须依赖App壳才能运行

  • 所有的公共配置都放在app.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
//公共的依赖 derry.gradle 文件

// 扩展块
ext {
// 正式环境 和 测试环境
isRelease = false

// 正式环境 和 测试环境 服务器 URL 配置
url = [
"debug" : "https://192.188.22.99/debug",
"release": "https://192.188.22.99/release"
]

// 建立Map存储, key 和 value 都是自定义的
androidID = [
compileSdkVersion : 30,
buildToolsVersion : "30.0.1",

applicationId : "com.derry.derry",
minSdkVersion : 16,
targetSdkVersion : 30,
versionCode : 1,
versionName : "1.0",

testInstrumentationRunner: "androidx.test.runner.AndroidJUnitRunner"
]

// 建立Map存储, key 和 value 都是自定义的
appID = [
app: "com.derry.modularproject",
login: "com.derry.login",
register: "com.derry.register"
]

// 300 行 MAP key value
dependenciesID = [
"appcompat" : "androidx.appcompat:appcompat:1.2.0",
"constraintlayout": "androidx.constraintlayout:constraintlayout:2.0.1",
"material" : "com.google.android.material:material:1.1.0",
]

}
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
//project 根目录下的 build.gradle
// 根目录下的build.gradle 引入 公共的一份 引入过来
apply from : 'derry.gradle'

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.0.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
}
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
//某个library
apply plugin: 'com.android.library'

println "Derry ---> lib Student hao 2"

android {
compileSdkVersion androidID.compileSdkVersion
buildToolsVersion androidID.buildToolsVersion

defaultConfig {
minSdkVersion androidID.minSdkVersion
targetSdkVersion androidID.targetSdkVersion
versionCode androidID.versionCode
versionName androidID.versionName

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

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

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

/*implementation "androidx.appcompat:appcompat:1.2.0"
implementation "androidx.constraintlayout:constraintlayout:2.0.1"
implementation "com.google.android.material:material:1.1.0"
implementation "androidx.vectordrawable:vectordrawable:1.1.0"
implementation "androidx.navigation:navigation-fragment:2.2.2"
implementation "androidx.navigation:navigation-ui:2.2.2"
implementation "androidx.lifecycle:lifecycle-extensions:2.2.0"*/

// 一行搞定300行 循环搞定
dependenciesID.each {k,v -> implementation v}

testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

}
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
//login这个 module 的 build.gradle

// apply plugin: 'com.android.application'
if (isRelease) { // 如果是发布版本时,各个模块都不能独立运行
apply plugin: 'com.android.library' // 正式环境 library不能独立运行
} else {
apply plugin: 'com.android.application' // 测试环境 application独立运行
}

android {
compileSdkVersion 30
buildToolsVersion "30.0.1"

defaultConfig {
// applicationId "" // 有appid 能够独立运行

if (!isRelease) { // 能够独立运行 必须要有appID
applicationId appID.login // 组件化模式能独立运行才能有applicationId
}

minSdkVersion 16
targetSdkVersion 30
versionCode 1
versionName "1.0"

testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

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

sourceSets {
main {
if (!isRelease) {
// 如果是组件化模式,需要单独运行时 Debug
manifest.srcFile 'src/main/debug/AndroidManifest.xml' // 生效
} else { // 正式环境下
// 集成化模式,整个项目打包apk
manifest.srcFile 'src/main/AndroidManifest.xml' // 让我们之前 默认的路径下的清单文件再次生效

java {
// 减小包大小,release 时 debug 目录下文件不需要合并到主工程
exclude "**/debug/**"
}
}
}
}
}

dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
implementation 'androidx.appcompat:appcompat:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.2'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'

}

上面代码可能有一些重复的,每个 gradle 文件可能侧重某一个点,需要注意甄别。

三、组件之间的通信方式

如果订单模块想要访问个人模块的信息,那我们必须要有个什么注册表,我们将这些功能注册到这个注册表中,需要调用的时候,就通过这个注册表。

组件化几种可行的通信方式:

  • EventBus:缺点是EventBean 的维护成本太高,不好管理

  • 广播:不好管理,都统一发送出去了,并且后续Android版本广播都需要动态注册了(这点存疑,需要验证)

  • 使用隐式意图:这个就更麻烦了,要求每个 Activity 都必须有自己唯一的action 名字

  • 类加载方式:容易写错包名的类,相对而言缺点较少,可以尝试

  • 使用全局 Map :因为所有的 module 都需要依赖公共基础库,所以可以在公共基础库中添加一个 Map ,注册所有的Activity, 需要注册很多对象,相对而言缺点少,可以尝试

其中类加载和 全局 Map 的方式使用代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public void jumpPersonal(View view) {
// todo 方式一 类加载
// 类加载跳转,可以成功。维护成本较高且容易出现人为失误
try {
Class targetClass = Class.forName("com.xiangxue.personal.Personal_MainActivity");
Intent intent = new Intent(this, targetClass);
intent.putExtra("name", "derry");
startActivity(intent);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}

// personal/Personal_MainActivity getMap
// todo 方式二 全局Map
Class<?> targetActivity =
RecordPathManager.startTargetActivity("personal", "Personal_MainActivity");
startActivity(new Intent(this, targetActivity));
}

需要注意的一点是,使用类加载方式,我们使用的是 Class.forName 的方式,这个是反射吗?这并不是反射,这只是类加载!反射我们是指反射属性,方法等,我们也不会看到导入的包里面有 Reflect 等反射的包名

当然,使用全局 Map 的方式必须还要在 Common 公共依赖里面有管理类:

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
/**
* 全局路径记录器(根据子模块进行分组)
*
* 组名:app,order,personal
* 详情order=[Order_MainActivity,Order_MainActivity2,Order_MainActivity3]
*
*/
public class RecordPathManager {

/**
* 先理解成 仓库
* group: app,order,personal
*
* order:
* OrderMainActivity1
* OrderMainActivity2
* OrderMainActivity3
*/
private static Map<String, List<PathBean>> maps = new HashMap<>();

/**
* 将路径信息加入全局Map
*
* @param groupName 组名,如:"personal"
* @param pathName 路劲名,如:"Personal_MainActivity"
* @param clazz 类对象,如:Personal_MainActivity.class
*/
public static void addGroupInfo(String groupName, String pathName, Class<?> clazz) {
List<PathBean> list = maps.get(groupName);

if (null == list) {
list = new ArrayList<>();
list.add(new PathBean(pathName, clazz));
// 存入仓库
maps.put(groupName, list);
} else {
// 存入仓库
maps.put(groupName, list);
}
}

/**
* 只需要告诉我,组名 ,路径名, 就能返回 "要跳转的Class"
* @param groupName 组名 oder
* @param pathName 路径名 OrderMainActivity1
* @return 跳转目标的class类对象
*/
public static Class<?> startTargetActivity(String groupName, String pathName) {
List<PathBean> list = maps.get(groupName);
if (list == null) {
Log.d(Config.TAG, "startTargetActivity 此组名得到的信息,并没有注册进来哦...");
return null;
}
// 遍历 寻找 去匹配 “PathBean”对象
for (PathBean pathBean : list) {
if (pathName.equalsIgnoreCase(pathBean.getPath())) {
return pathBean.getClazz();
}
}
return null;
}
}

然后,我们可以在各个 Module 里面需要注册当前 Module 所拥有的全部 Activity (当然,这个并不优雅,开发者可能会忘记,可以采用注解或者其他的方式去做,这里只是简单实现):

1
2
3
4
5
6
7
8
9
10
11
12
public class AppApplication extends BaseApplication {

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

// 如果项目有100个Activity,这种加法会不会太那个? 缺点
RecordPathManager.addGroupInfo("app", "MainActivity", MainActivity.class);
RecordPathManager.addGroupInfo("order", "Order_MainActivity", Order_MainActivity.class);
RecordPathManager.addGroupInfo("personal", "Personal_MainActivity", Personal_MainActivity.class);
}
}

所以,全局Map 的方案跳转的时候,只需要标明你想跳转哪个module ,以及module 中的哪个 Activity。但是上述方式还是有点麻烦,这时候,阿里开源的 Arouter就应运而生了

组件化通信框架很多,但是目前最优秀的是 ARouter 。

组件化:模块之间没有依赖,便于重用

插件化:侧重动态化加载某些功能,主要问题是兼容性问题,支付宝都放弃了,因为你兼容了 5.0 ,能兼容 11.0 吗

模块化:模块化是业务层面的拆分,组件化是功能层次的划分

Google 表态说是 FrameWork 仍然还是 Java ,不会改成 Kotlin

谢谢你的鼓励