前言
组件化技术,在 Android 开发中有着举足轻重的作用。
随着时间推移,软件项目很多都会变得越来越庞杂。此时,采用组件化技术,对项目进行改造,是一种较优的方案。
谈谈模块化
要聊组件化,惯例是要谈谈模块化的,毕竟它与组件化确实有一些相同点,在组件化的项目中它也会与组件化发生关联。
什么是模块化
模块化开发,是每个开发者都熟悉的。
即将常用的UI、网络请求、数据库操作、第三方库的使用等公共部分抽离封装成基础模块,或者将大的业务上拆分为多个小的业务模块,这些业务模块又依赖于公共基础模块的开发方式。
更宏观上,又会将这些不同的模块组合为一个整体,打包成一个完成的项目。
模块化的好处
模块化有哪些好处呢?
复用
首先,基础模块,可为业务模块所复用;
其次,子业务模块,可为父业务模块,甚至不同的项目所复用。
解耦
降低模块间的耦合,避免出现一处代码修改,牵一发而动全身的尴尬局面。
协同开发
项目越来越大,团队人数越来越多,模块化开发可在尽量解耦的情况下,使不同的开发人员专注于自己负责的业务,同步开发,显著提供开发效率。
模块化的弊端
那,模块化开发有没有什么弊端呢?
有。
任凭模块化做得多么好,还是跳不出是组合在单一项目下的。随着项目的发展与迭代,模块化开发渐渐显现了以下的问题:
项目代码量越来越大
每次的编译速度越来越慢,哪怕几行代码的修改,都需要花费好几分钟的时间,等着编译器编译运行结束后,才能查看代码的执行结果,这极大的降低了开发效率;
业务模块越来越多
不可避免地产生越来越多且复杂的耦合,哪怕一次小的功能更新,也需要对修改代码耦合的模块进行充分测试;
团队人数越来越多
这就要求开发人员了解与之业务相关的每一个业务模块,防止出现某位开发人员修改代码导致其他模块出现 bug 的情况,这个要求对于开发人员显然是不友好的;
那怎样解决模块化开发的这些弊端呢?
当然是组件化喽!
聊聊组件化
组件化可以说是 Android 中级开发工程师必备技能了,能有效解决许多单一项目下开发中出现的问题。
并且我要强调的是,组件化真的不难,还没搞过的小伙伴不要怂。
什么是组件化
组件,顾名思义,“组装的零件”,术语上叫做软件单元,可用于组装在应用程序中。
所以,组件化,要更关注可复用性、更注重关注点分离、功能单一、高内聚、粒度更小、是业务上能划分的最小单元,毕竟是“组装的零件”嘛!
从这个角度上看,组件化的粒度,似乎要比模块化的粒度更小。
不过,我个人认为,要把组件化拆分到如此小的粒度,不可能,也没有必要。在组件化项目的实际开发中,组件化的粒度,是要比模块化的粒度更大的。
组件化的好处
首先要说的是,上述模块化的好处,组件化都有,不再赘述;上述模块化的弊端,组件化都给解决了,具体如下:
组件,既可以作为 library,又可以单独作为 application,便于单独编译单独测试,大大的提高了编译和开发效率;
(业务)组件,可有自己独立的版本,业务线互不干扰,可单独编译、测试、打包、部署;
各业务线共有的公共模块可开发为组件,作为依赖库供各业务线调用,减少重复代码编写,减少冗余,便于维护;
通过 gradle 配置文件,可对第三方库进行统一管理,避免版本冲突,减少冗余;
通过 gradle 配置文件,可实现 application 与 library 灵活组合与拆分,可以更快速的响应需求方对功能模块的选择。
组件化实践
首先要说明的是,下述是一个简单的不能再简单的组件化案例,只求帮助大家搭建起组件化的架构,功能上极其简约。
九层之台,起于累土。我们这就开始搭组件化的架构吧!
组件化架构
先上一张组件化项目整体架构图其中的“业务组件”,既可以作为 application 单独打包为 apk,又可以作为 library 灵活组合为综合一些的应用程序。
大多数开发者做组件化时面对的业务需求,都是上面这种情况。
我司的需求略有不同,不是将子业务组件组合为整体应用程序,而是反其道而行之,需要将已上线项目拆分给不同的业务公司使用,在不同业务系统中,项目的逻辑和代码会有区别,且版本不统一。
基于此,我搭建项目架构如下图所示,其中“m_moudle_main”是公司主要的、且逻辑和代码相同的业务组件,“b_moudle_north”和“b_moudle_south”是拆分出来的业务组件,管理各自私有的逻辑和代码,且版本有差别。从Android工程看,结构如下图所示:
注:取moudle名,手动加上“b_” “m_” “x_”这样的前缀,只是为了便于分辨组件层次。
统一配置文件
在项目根目录下,自建 config.gradle 文件,对项目进行全局统一配置,并对版本和依赖进行统一管理,源码如下:
/**
* 全局统一配置
*/
ext {
/**
* module开关统一声明在此处
* true:module作为application,可单独打包为apk
* false:module作为library,可作为宿主application的组件
*/
isNorthModule = false
isSouthModule = false
/**
* 版本统一管理
*/
versions = [
applicationId : "com.niujiaojian.amd", //应用ID
versionCode : 100, //版本号
versionName : "1.0.0", //版本名称
compileSdkVersion : 28,
minSdkVersion : 21,
targetSdkVersion : 28,
androidSupportSdkVersion: "28.0.0",
constraintlayoutVersion : "1.1.3",
runnerVersion : "1.1.0-alpha4",
espressoVersion : "3.1.0-alpha4",
junitVersion : "4.12",
annotationsVersion : "28.0.0",
appcompatVersion : "1.0.0-beta01",
designVersion : "1.0.0-beta01",
multidexVersion : "1.0.2",
butterknifeVersion : "10.1.0",
arouterApiVersion : "1.4.1",
arouterCompilerVersion : "1.2.2",
arouterAnnotationVersion: "1.0.4"
]
dependencies = [
"appcompat" : "androidx.appcompat:appcompat:${versions["appcompatVersion"]}",
"constraintlayout" : "androidx.constraintlayout:constraintlayout:${versions["constraintlayoutVersion"]}",
"runner" : "androidx.test:runner:${versions["runnerVersion"]}",
"espresso_core" : "androidx.test.espresso:espresso-core:${versions["espressoVersion"]}",
"junit" : "junit:junit:${versions["junitVersion"]}",
//注释处理器
"support_annotations" : "com.android.support:support-annotations:${versions["annotationsVersion"]}",
"design" : "com.google.android.material:material:${versions["designVersion"]}",
//方法数超过65535解决方法64K MultiDex分包方法
"multidex" : "androidx.multidex:multidex:2.0.0",
//阿里路由
"arouter_api" : "com.alibaba:arouter-api:${versions["arouterApiVersion"]}",
"arouter_compiler" : "com.alibaba:arouter-compiler:${versions["arouterCompilerVersion"]}",
"arouter_annotation" : "com.alibaba:arouter-annotation:${versions["arouterAnnotationVersion"]}",
//黄油刀
"butterknife" : "com.jakewharton:butterknife:${versions["butterknifeVersion"]}",
"butterknife_compiler": "com.jakewharton:butterknife-compiler:${versions["butterknifeVersion"]}"
]
}
复制代码
然后在project的build.gradle中引入config.gradle文件:
apply from: "config.gradle"
复制代码
基础公共组件
基础公共组件 common 将一直作为 library 存在,所有业务组件都需要依赖 common 组件。
common 组件主要负责封装公共部分,如网络请求、数据存储、自定义控件、各种工具类等,以及对第三方库进行统一依赖等。
下图是我的 common 组件的包结构图:
前文有言,common 组件还负责对第三方库进行统一依赖,这样上层业务组件就不需要再对第三方库进行重复依赖了,其 build.gradle 源码如下所示:
apply plugin: 'com.android.library'
apply plugin: 'com.jakewharton.butterknife'
……
dependencies {
// 在项目中的libs中的所有的.jar结尾的文件,都是依赖
implementation fileTree(dir: 'libs', include: ['*.jar'])
//把implementation 用api代替,它是对外部公开的, 所有其他的module就不需要添加该依赖
api rootProject.ext.dependencies["appcompat"]
api rootProject.ext.dependencies["constraintlayout"]
api rootProject.ext.dependencies["junit"]
api rootProject.ext.dependencies["runner"]
api rootProject.ext.dependencies["espresso_core"]
//注释处理器,butterknife所必需
api rootProject.ext.dependencies["support_annotations"]
//MultiDex分包方法
api rootProject.ext.dependencies["multidex"]
//Material design
api rootProject.ext.dependencies["design"]
//黄油刀
api rootProject.ext.dependencies["butterknife"]
annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]
//Arouter路由
annotationProcessor rootProject.ext.dependencies["arouter_compiler"]
api rootProject.ext.dependencies["arouter_api"]
api rootProject.ext.dependencies["arouter_annotation"]
}
复制代码
业务组件
业务组件在 library 模式下,向上组合为整体性项目;在 application 模式下,可独立运行。
其 build.gradle 源码如下:
if (Boolean.valueOf(rootProject.ext.isModule_North)) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
apply plugin: 'com.jakewharton.butterknife'
……
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
//公用依赖库
implementation project(':x_module_common')
implementation project(':m_module_main')
//黄油刀
annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]
//Arouter路由
annotationProcessor rootProject.ext.dependencies["arouter_compiler"]
}
复制代码
至此,组件化架构的搭建就算完成了。
可还有几个问题,是组件化开发中必须要关注的,也是项目做组件化改造时可能会遭遇的难点,我们一起来看看吧。
组件化必须要关注的几个问题
Application
在 common 组件中有 BaseAppliaction,提供全局唯一的 context,上层业务组件在组件化模式下,均需继承于 BaseAppliaction。
/**
* 基础 Application,所有需要模块化开发的 module 都需要继承自此 BaseApplication。
*/
public class BaseApplication extends Application {
//全局唯一的context
private static BaseApplication application;
@Override
protected void attachBaseContext(Context base) {
super.attachBaseContext(base);
application = this;
//MultiDexf分包初始化,必须最先初始化
MultiDex.install(this);
}
@Override
public void onCreate() {
super.onCreate();
initARouter();
}
/**
* 初始化路由
*/
private void initARouter() {
if (BuildConfig.DEBUG) {
ARouter.openLog(); // 打印日志
ARouter.openDebug(); // 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
}
ARouter.init(application);// 尽可能早,推荐在Application中初始化
}
/**
* 获取全局唯一上下文
*
* @return BaseApplication
*/
public static BaseApplication getApplication() {
return application;
}
复制代码
applicationId 管理
可为不同组件设置不同的 applicationId,也可缺省,在Android Studio中,默认的 applicationId 与包名一致。
组件的 applicationId 在其 build.gradle 文件的 defaultConfig 中进行配置:
if (Boolean.valueOf(rootProject.ext.isModule_North)) {
//组件模式下设置applicationId
applicationId "com.niujiaojian.amd.north"
}
复制代码
manifest.xml 管理
组件在 library 模式和 application 模式下,需要配置不同的 manifest.xml 文件,因为在 application 模式下,程序入口 Activity 和自定义的 Application 是不可或缺的。
在组件的 build.gradle文件 的 android 中进行 manifest 的管理:
/*
* java插件引入了一个概念叫做SourceSets,通过修改SourceSets中的属性,
* 可以指定哪些源文件(或文件夹下的源文件)要被编译,
* 哪些源文件要被排除。
* */
sourceSets {
main {
if (Boolean.valueOf(rootProject.ext.isModule_North)) {//apk
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
java {
//library模式下,排除java/debug文件夹下的所有文件
exclude '*module'
}
}
}
}
复制代码
资源名冲突问题
资源名冲突问题,相信大家多多少少都遇到过,以前最常见的就是第三方 sdk 导致的资源名冲突了。
这个问题没有特别好的解决办法,只能通过设置资源名前缀 resourcePrefix
以及约束自己开发习惯进行解决。
资源名前缀 resourcePrefix
,是在 Project 的 build.gradle 中进行设置的:
/**
* 限定所有子类xml中的资源文件的前缀
* 注意:图片资源,限定失效,需要手动添加前缀
* */
subprojects {
afterEvaluate {
android {
resourcePrefix "${project.name}_"
}
}
}
复制代码
这样设置完之后,string、style、color、dimens 等中资源名,必须以设置的字符串为前缀,而 layout、drawable 文件夹下的 shape 的 xml 文件的命名,必须以设置的字符串为前缀,否则会报错提示。
另外,资源前缀的设置对图片的命名无法限定,建议大家约束自己的开发习惯,自觉加上前缀。
建议:将 color、shape、style 这些放在基础库组件中去,这些资源不会太多,且复用性极高,所有业务组件又都会依赖基础库组件。
Butterknife R2 问题
Butterknife 存在的问题是控件 id 找不到,只要将 R 替换为 R2 即可解决问题。
需要注意的是,在如下代码示例外的位置,不要这样做,保持使用 R 即可,如 setContentView(R.layout.b_module_north_activity_splash)
public class SplashActivity extends BaseActivity {
@BindView(R2.id.btn_toMain)
Button btnToMain;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.b_module_north_activity_splash);
ButterKnife.bind(this);
}
……
@OnClick(R2.id.btn_toMain)
public void onViewClicked() {
}
}
复制代码
另外要注意的是,每一个使用 Butterknife 的组件,在其 build.gradle 的 dependencies 都要配置注解处理器处理其 compiler 库:
apply plugin: 'com.jakewharton.butterknife'
……
dependencies {
……
annotationProcessor rootProject.ext.dependencies["butterknife_compiler"]
}
复制代码
组件间跳转
由于业务组件间不存在依赖关系,不可以通过 Intent 进行显式跳转。
若需跳转,是要借助于路由的,我使用的是阿里的开源框架 ARouter
。
注:我在案例中只使用了 ARouter 的基础的页面跳转功能,更复杂的诸如携带参数跳转、声明拦截器等功能的使用方法,大家可到 Github 上查看其使用文档。
在每一个需要用到 ARouter 的组件的 build.gradle 文件中对其进行配置:
android {
...
defaultConfig {
...
//Arouter路由配置
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
includeCompileClasspath = true
}
}
}
}
dependencies{
...
//Arouter路由
annotationProcessor rootProject.ext.dependencies["arouter_compiler"]
}
复制代码
跳转目标页面配置:
@Route(path = "/main/MainActivity")
public class MainActivity extends BaseActivity {
……
}
复制代码
跳转来源页面的跳转代码:
...
ARouter.getInstance()
.build("/main/MainActivity")
.navigation();
...
复制代码
后记
组件化优势多多,用起来爽的不要不要的。
其中快感来的最快的,当属大大提升了编译速度了。
最后的话我整理了一套组件化学习笔记及视频,有需要的同学可以在这里自取。