一、概述
1.1 组件化的意义
组件化是大型 app 的标配,它可以帮助解决项目开发过程中可能会遇到的一些问题,如:
- 代码耦合:项目增大后易失去层次感,容易出现不同业务间的代码互相调用,高度耦合。组件化则可以实现各模块间不相互依赖,但可以互相交互、任意组合,高度解耦。
- 编译时间长:项目代码越多编译时间越长。而组件化可以分模块打包进行编译测试。
- 代码复用率低:不同业务间可能会出现重复的基础代码,但是并没有被抽离出来进行复用。组件化可以将基础组件或功能抽离出来进行复用(到新项目)。
- 团队开发效率低:多人协作开发时,可能会由于代码风格不同而互相影响,也可能会增加代码版本管理成本或沟通成本。组件化将功能按照模块划分后可以一定程度上减轻以上问题从而提高效率。
1.2 组件化的成员
项目使用组件化后的大致构成:
使用组件化后,app 模块除了一些全局配置以及应用入口外,就不再包含具体的业务代码,也被称作 app 壳,它依赖于各个业务模块。
而各个业务模块(home、product、order、personal)之间平起平坐,不会相互依赖,但是可以通过路由进行通信,同时也可以单独打包出一个 apk 进行单独测试。
公共基础库会提供一些通用功能供上层,比如 Base 可以封装 BaseActivity/BaseFragment,Utils 工具类,Retrofit + OKHttp + RxJava 的网络访问组件以及提供组件化路由的 ARouter 等等。这部分在实际项目中可能会再细分出多个层次,需要根据项目灵活变化,由于我们聚焦的是组件化模块和路由,所以这部分就直接粗略的用一层表示了。
1.3 环境切换
组件化项目通常有两种环境,一种是组件化环境,一种是集成化环境:
- 组件化环境:子模块能独立运行,可以独立打包出 apk。
- 集成化环境:子模块不能独立运行,需要整个项目打包成一个 apk。
两种环境的切换通常是通过配置 Gradle 实现的,组件化模式时,app 以及子模块都是 Phone Module,子模块才能单独打包出 apk;而集成化模式时,只有 app 是 Phone Module,子模块需要配置成 Android Library:
观察 Phone Module 与 Android Library 的 build.gradle 文件,找出二者的区别,也就找到了在二者间切换的方法:
- Phone Module 应用的插件是 com.android.application,而 Android Library 则是 com.android.library。
- Phone Module 会在 android -> defaultConfig 节点下声明 applicationId,而 Android Library 则没有。
所以在配置子模块的 gradle 时,可以使用类似如下的方式实现环境切换:
// isRelease 表示是否为集成化模式
if (isRelease) {
apply plugin: 'com.android.library'
} else {
apply plugin: 'com.android.application'
}
android {
defaultConfig {
if (!isRelease) {
applicationId com.xxx.xxxx
}
}
完整的配置方式会在下一节介绍。
二、基本配置
2.1 创建模块
创建子模块 order 和 personal,选择 Phone & Tablet Module 好一点,因为 AS 会创建默认的 Activity 以及 res 目录,选择 Android Library 则不会(总结:选 Phone & Tablet Module 省事儿)。项目结构图中的【公共基础库】部分,我们就新建一个 Android Library 的 common 模块来模拟了:
2.2 配置模块的 build.gradle
然后配置各个模块下的 build.gradle 以及整个项目的 build.gradle,我们的想法是将子模块中相同的内容向上抽取到项目的 build.gradle 中(便于统一管理),再通过配置文件中的变量控制集成化与组件化的切换。在项目根目录下新建 config.gradle 文件,内容如下:
ext {
// 表示是否为正式版本,即集成化环境
isRelease = true
// 建立 Map,以键值对形式存储版本信息变量
androidId = [
compileSdkVersion: 32,
buildToolsVersion: "29.0.3",
minSdkVersion : 23,
targetSdkVersion : 32,
versionCode : 1,
versionName : "1.0"
]
// 所有模块的 Id
appId = [
app : "com.demo.arouter",
order : "com.demo.order",
personal: "com.demo.personal"
]
// 所有模块都使用的依赖才存在这里
dependencies = [
"appcompat" : "androidx.appcompat:appcompat:1.3.1",
"constraint": "androidx.constraintlayout:constraintlayout:2.0.4"
]
}
isRelease 属性其实就是切换组件化与集成化环境的开关。此外 config.gradle 文件需要在项目的 build.gradle 中引入才能生效:
apply from: 'config.gradle'
接下来才开始配置模块的 build.gradle,由于 app 模块与其它子模块还是有不同之处的:
- app 一直是一个 Phone & Tablet Module,即便是集成化环境时它也不会切换成 Android Library;
- app 在集成化环境时需要依赖其它业务子模块(在当前例子中就是要依赖 order 和 personal 模块)。
所以先看一下 app 的基本配置:
apply plugin: 'com.android.application'
def androidId = rootProject.ext.androidId
def appId = rootProject.ext.appId
def support = rootProject.ext.dependencies
android {
compileSdkVersion androidId.compileSdkVersion
buildToolsVersion androidId.buildToolsVersion
defaultConfig {
applicationId appId.app
minSdkVersion androidId.minSdkVersion
targetSdkVersion androidId.targetSdkVersion
versionCode androidId.versionCode
versionName androidId.versionName
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
// 应用所有在 config.gradle 中定义的依赖
support.each { k, v -> implementation v }
// 集成化模式,要发布版本时,子模块就都不能独立运行了
if (isRelease) {
implementation project(":module-order")
implementation project(":module-personal")
}
implementation project(":lib-common")
}
注意一下应用依赖所使用的方式。前面我们在 config.gradle 中将所有模块共同需要依赖的库以键值对的形式保存在 Map dependencies 中,并且在 app 模块的 build.gradle 文件中取出 dependencies 并赋值给变量 support,each 实际上是 groovy 中提供的一个方法,作用相当于 Java 中的 foreach,即遍历集合中的所有元素。因此 support.each { k, v -> implementation v } 就是遍历 support 中的元素,并对每个元素执行 implementation value。
config.gradle 中定义的属性也可以以键值对的形式直接定义在 gradle.properties 文件中。
其它子模块的配置与 app 略有不同:
// 切换集成化环境与组件化环境
if (isRelease) {
apply plugin: 'com.android.library'
} else {
apply plugin: 'com.android.application'
}
def androidId = rootProject.ext.androidId
def appId = rootProject.ext.appId
def support = rootProject.ext.dependencies
android {
compileSdkVersion androidId.compileSdkVersion
buildToolsVersion androidId.buildToolsVersion
defaultConfig {
// 只有组件化环境时才需要执行 applicationId
if (!isRelease) {
applicationId appId.order
}
minSdkVersion androidId.minSdkVersion
targetSdkVersion androidId.targetSdkVersion
versionCode androidId.versionCode
versionName androidId.versionName
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
if (!isRelease) {
// 测试版本,组件化环境,让 /main/debug/ 下的 AndroidManifest 生效
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
// 正式版本,集成化环境,让 main 下的 AndroidManifest 生效
manifest.srcFile 'src/main/AndroidManifest.xml'
java {
// release 时 debug 目录下的文件不需要合并到主工程中,减小 apk 体积
exclude '**/debug/**'
}
}
}
}
}
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
support.each { k, v -> implementation v }
implementation project(":lib-common")
}
当处于集成化环境时,整个应用打包成一个 apk,程序入口在 app 模块中,但当切换成组件化模式时,子模块单独打包为一个 apk,需要指定一个入口 Activity,并且配置 Application 标签下的相关内容,因此采用通过 sourceSets 指定源集的方式指定不同环境下所使用的 AndroidManifest 文件。
到这里,组件化项目的基本配置就完成了。
三、组件间通信
组件化后,不同组件之间没有相互依赖,模块间的跳转就不能再通过 startActivity() 这种方式,比较常用的是阿里的 ARouter 路由框架,GitHub 的项目主页上对于使用方法介绍的已经很详细,这里我们就简单说说。
3.1 添加 ARouter 依赖与配置
官方给出的依赖与配置是这样的:
android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}
dependencies {
// Replace with the latest version
compile 'com.alibaba:arouter-api:?'
annotationProcessor 'com.alibaba:arouter-compiler:?'
...
}
适用到我们当前的例子中,就是公共基础库的 common 模块的 build.gradle,以如下方式添加 arouter-api 的依赖:
dependencies {
...
api 'com.alibaba:arouter-api:1.5.2'
}
而 app 及其他子模块添加注解处理器和模块名参数:
android {
...
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
// 当前模块名作为参数
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}
dependencies {
...
// 注解处理器的依赖
annotationProcessor 'com.alibaba:arouter-compiler:1.5.2'
}
因为 ARouter 是利用 APT 技术通过自动生成代码建立路由表从而支持模块间跳转的,因此模块中如果有 Activity/Fragment 需要加入路由表时,就需要为其加上注解,并由注解处理器扫描后才能被添加到路由表中,而建立路由表时需要用模块名作为参数,所以才需要用 javaCompileOptions -> annotationProcessorOptions 这种形式将模块名传递给注解处理器。
3.2 添加注解
给需要加入路由表的 Activity 添加 @Route 注解:
@Route(path = "/app/MainActivity")
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
}
@Route 注解的 path 值最好按照【/模块名/Activity名】的方式写,打上这个注解的 Activity 会被添加到路由表中,进而可以被跳转。
3.3 初始化 SDK
public class BaseApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
if (BuildConfig.DEBUG) {
ARouter.openLog();
ARouter.openDebug();
}
ARouter.init(this);
}
}
初始化完成后,编译项目,会发现 ARouter 在各个模块下为我们生成了一些文件,正是这些文件帮我们完成了路由表的建立等工作:
当前先不用理解这些文件的生成规则与具体内容,后面文章会详细说明。
3.4 使用 ARouter 进行跳转
最基本的跳转功能:
public void jumpToOrder(View view) {
ARouter.getInstance().build("/order/Order_MainActivity").navigation();
}
build() 中传的是被跳转的页面的路由地址:
@Route(path = "/order/Order_MainActivity")
public class Order_MainActivity extends AppCompatActivity {
...
}
也可以带参数跳转,假如从 Order 跳转到 Personal:
@Route(path = "/order/Order_MainActivity")
public class Order_MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_order_main);
}
public void jumpToPersonal(View view) {
ARouter.getInstance().build("/personal/Personal_MainActivity")
.withString("key1", "TestString")
.withInt("key2", 66)
.navigation();
}
}
接收方的对应变量要打上 @AutoWired 注解,默认情况下,变量名就是参数传递的 key,或者也可以通过 name 指定:
@Route(path = "/personal/Personal_MainActivity")
public class Personal_MainActivity extends AppCompatActivity {
@Autowired
String key1;
@Autowired(name = "key2")
int count;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_personal_main);
// 如果想通过依赖注入自动接收参数值,要调用这个方法。
ARouter.getInstance().inject(this);
Toast.makeText(this, "key1 = " + key1 + ",count = " + count, Toast.LENGTH_LONG).show();
}
}
两次跳转的效果图如下:
3.5 使用 ARouter 调用其它模块服务
假如,现在有一个需求,要在 personal 模块的页面上显示待发货、待收货、待评价的订单数量,理论上来说这些订单信息需要由 order 模块对外提供服务,再由 personal 模块调用这些服务以获取订单信息。由于子模块之间不存在依赖,因此不能直接调用,需要借助 ARouter 路由来实现。
首先,在 common 模块中定义接口 IOrderService,继承自 IProvider:
public interface IOrderService extends IProvider {
int getBacklogCount();
int getSendingCount();
int getEvaluatingCount();
}
然后在 order 模块中实现该接口,并把实现类通过 @Route 注解添加进路由中:
@Route(path = "/order/OrderServiceImpl")
public class OrderServiceImpl implements IOrderService {
@Override
public int getBacklogCount() {
return 4;
}
@Override
public int getSendingCount() {
return 3;
}
@Override
public int getEvaluatingCount() {
return 5;
}
@Override
public void init(Context context) {
}
}
最后在 personal 模块中通过依赖注入获取到 order 模块提供的 IOrderService 服务,调用相关方法:
@Route(path = "/personal/Personal_MainActivity")
public class Personal_MainActivity extends AppCompatActivity {
@Autowired
IOrderService orderService;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_personal_main);
ARouter.getInstance().inject(this);
int backlogCount = orderService.getBacklogCount();
int sendingCount = orderService.getSendingCount();
int evaluatingCount = orderService.getEvaluatingCount();
Toast.makeText(this, "待发货:" + backlogCount + ",待收货:" + sendingCount +
",待评价:" + evaluatingCount, Toast.LENGTH_LONG).show();
}
}
效果图:
到这里,一个特别基础的组件化项目就算搭建完成了,下篇文章会模仿 ARouter 实现一个简单的路由框架:Android 组件化基础(二)—— 仿 ARouter 实现一个路由框架