前言
随着项目业务不断增多,功能越来越复杂,各个业务代码耦合越来越多,代码量急剧增加,难以高效开发与管理
- 各模块耦合严重,相互引用实现
- 修改旧模块,难以分析对其他模块影响
- 新开业务,必须在整个工程中引入,多个开发人员存在同步问题
- 难以做单元测试,测试某个模块,需要在整个工程中测试,编译时间过长
模块
将一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,比如登录模块、首页模块等等。组件化
组件指的是单一的功能组件,如登录组件、视频组件、支付组件 等,每个组件都可以以一个单独的 module 开发,并且可以单独抽出来作为 SDK 对外发布使用。可以说往往一个模块包含了一个或多个组件。
根据业务和职责将整个工程拆分成各个组件,然后以某种方式解决组件间的依赖关系,做到单一职责、高内聚、低耦合。
最终效果:每个业务组件都是一个微型App,SDK。组件化的优势
组件化基于可重用的目的,将应用拆分成多个独立组件,以减少耦合:
- 加快编译速度:每个业务功能都是一个单独的工程,可独立编译运行,拆分后代码量较少,编译自然变快。
- 解耦:通过关注点分离的形式,将App分离成多个模块,每个模块都是一个组件。
- 提高开发效率:多人开发中,每个组件模块由单人负责,降低了开发之间沟通的成本,减少因代码风格不一而产生的相互影响。
代码复用:类似我们引用的第三方库,可以将基础组件或功能组件剥离。在新项目微调或直接使用。
组件化分层架构
组件依赖关系是上层依赖下层,修改频率是上层高于下层。先上一张图:
基础组件
基础公共模块,最底层的库:
封装公用的基础组件;
- 网络访问框架、图片加载框架等主流的第三方库;
-
common 组件
支撑业务组件、功能组件的基础(BaseActivity/BaseFragment等基础能力;
- 依赖基础组件层;
业务组件、功能组件所需的基础能力只需要依赖common组件即可获得。
功能组件
依赖基础组件层;
- 对一些公用的功能业务进行封装与实现;
业务组件可以在library和application之间切换,但是最后打包时必须是library ;
业务组件
可直接依赖基础组件层;同时也能依赖公用的一些功能组件;
- 各组件之间不存在依赖关系,通过路由进行通信;
业务组件可以在library和application之间切换,但是最后打包时必须是library ;
主工程(app)
只依赖各业务组件;
- 除了一些全局的配置和主Activity之外,不包含任何业务代码,是应用的入口;
要解决的问题
1. gradle 统一配置
这里比较合适的是 buildSrc 和includeBuild,相关使用方式可以参考版本统一依赖管理。2. 组件独立调试
一个组件可以根据需要,既可以是一个单独的module,可以单独调试。又可以是一个library,可以被其他组件依赖。
如何让组件在这两种调试模式之间自动转换呢? 手动修改组件的 gralde 文件,切换 Application 和 library ?如果项目只有两三个组件那么是可行的,但在大型项目中可能会有十几个业务组件,一个个手动修改显得费力笨拙。
我们知道用AndroidStudio创建一个Android项目后,会在根目录中生成一个gradle.properties文件。在这个文件定义的常量,可以被任何一个build.gradle读取。 所以我们可以在gradle.properties中定义一个常量值 isModule,true为即独立调试;false为集成调试。然后在业务组件的build.gradle中读取 isModule,设置成对应的插件即可。代码如下://gradle.properties
#组件独立调试开关, 每次更改值后要同步工程
isModule = false
//build.gradle
//注意gradle.properties中的数据类型都是String类型,使用其他数据类型需要自行转换
if (isModule.toBoolean()){
apply plugin: 'com.android.application'
}else {
apply plugin: 'com.android.library'
}
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;
}
4. Manifest.xml管理
- 一个 APP 是只有一个 ApplicationId ,所以在单独调试和集成调试组件的 ApplicationId 应该是不同的。
- 单独调试时也是需要有一个启动页,当集成调试时主工程和组件的AndroidManifest文件合并会产生多个启动页。
根据上面动态配制插件的经验,我们也需要在build.gradle中动态配制ApplicationId 和 AndroidManifest 文件。
5. 资源文件重名问题
资源文件命名冲突,看过很多解决方法,最常见的就是第三方 sdk 导致的资源名冲突了。
这个问题没有特别好的解决办法,只能通过设置资源名前缀 resourcePrefix 以及约束自己开发习惯进行解决。
资源名前缀 resourcePrefix ,是在 Project 的 build.gradle 中进行设置的:
/**
* 限定所有子类xml中的资源文件的前缀
* 注意:图片资源,限定失效,需要手动添加前缀
* */
subprojects {
afterEvaluate {
android {
resourcePrefix "${project.name}_"
}
}
}
这样设置完之后,string、style、color、dimens 等中资源名,必须以设置的字符串为前缀,而 layout、drawable 文件夹下的 shape 的 xml 文件的命名,必须以设置的字符串为前缀,否则会报错提示。
另外,资源前缀的设置对图片的命名无法限定,建议大家约束自己的开发习惯,自觉加上前缀。
上面的方法是对整个项目设置,如果想对单个module进行设置,在android 中设置 resourcePrefix 属性,前缀字符串根据实际情况自定义。设置方式如下:
android {
compileSdkVersion 28
resourcePrefix "app_"
}
建议:将 color、shape、style 这些放在基础库组件中去,这些资源不会太多,且复用性极高,所有业务组件又都会依赖基础库组件。
6. 界面跳转
前面说到,组件化的核心就是解耦,所以组件间是不能有依赖的,那么如何实现组件间的页面跳转呢?
例如 在首页模块 点击 购物车按钮 需要跳转到 购物车模块的购物车页面,两个模块之间没有依赖,也就说不能直接使用 显示启动 来打开购物车Activity,那么隐式启动呢? 隐式启动是可以实现跳转的,但是隐式 Intent 需要通过 AndroidManifest 配置和管理,协作开发显得比较麻烦。这里我们采用业界通用的方式—路由。
比较著名的路由框架 有阿里的ARouter、美团的WMRouter,它们原理基本是一致的。
这里我们采用使用更广泛的ARouter:“一个用于帮助 Android App 进行组件化改造的框架 —— 支持模块间的路由、通信、解耦”。使用方式可以参考官方文档。
7. 组件间通信
参考
Android 组件化最佳实践
Android 手把手带你搭建一个组件化项目架构
Android 组件化,从入门到不可自拔
“终于懂了” 系列:Android组件化,全面掌握! | 掘金技术征文-双节特别篇
Android 项目组件化详细实施方案