1.背景介绍
在移动端项目功能不断完善和丰富的过程中我们一直在寻找一种可以高效开发且复用率高的开发模式,特别是多应用同步开发、管理。
在开发过程中你是否遇到需要发布影子工程?新建项目是否需要耗费大量时间将原有基类、工具类、第三方通用类库逐个 copy 进新项目中?在项目不断迭代后是否发现编译时间不断增加?
组件化开发可以很好的解决上述问题,它实际是将一个完整的项目划分为若干个模块,过程类似搭积木,一个一个拼接,你可以单独使用其中任意一个,也可以用多个拼接出各种形状,达到高效开发,最大复用。
在了解组件化开发之前我们需要先了解组件和模块这两个概念
组件:指的是单一的功能组件,如地图组件(MapSDK)、支付组件(AnjukePay)、路由组件(Router)等等;
模块:指的是独立的业务模块,如基类模块(common-module)、商城模块(mall-module)、会员模块(member-module)等等;模块相对于组件来说粒度更大。
2.系统架构图
项目由主项目 app,商城模块 mall-module,会员模块 member-module,公共模块 common-module 各个基类(BaseActivity/BaseApplication/BaseAdapter 等)、工具类、网络、图片等一些公共常用的第三方 sdk:
app:作为项目的主程序入口,生产环境时可以引入其他模块如本项目的的 common-module、mall-modulem、member-module;开发调试不涉及到跨模块调用业务时可以单独引入 common-module 依赖运行。
mall-module:商城模块,编写完整的商城业务逻辑,生产环境作为 library 引入 app 主项目,开发阶段可以 application 方式单独运行,亦可作为商城应用独立发布。
member-module:会员模块,编写完整的会员业务逻辑,生产环境作为 library 引入 app 主项目,开发阶段可以 application 方式单独运行,亦可作为会员独立发布。
common-module:作为整个项目的基石,所有基类增加修改,均可以在引入项目中生效,达到最大复用率,提高开发效率。
3.实践
mall-module、member-module、common-module 作为 library 被引入 app 中运行效果图:
mall-module 作为应用单独运行效果图:
主项目 app 创建 项目 build.gradle 配置文件:
apply plugin: 'com.alibaba.arouter'
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:2.3.3'
classpath "com.alibaba:arouter-register:1.0.2"
}
}
allprojects {
repositories {
google()
jcenter { url "http://jcenter.bintray.com/" }
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
项目 gradle.properties 全局配置文件:
#IS_MAIN_APP true主项目为应用 mall-module为library false mall-module为应用可单独启动
IS_MAIN_APP=true
#AS 编译配置
COMPILE_SDK_VERSION=28
BUILDTOOLS_VERSION=28.0.3
MIN_SDK_VERSION=15
TARGET_SDK_VERSION=25
#版本号
APP_VERSION=1
APP_VERSION_CODE=1.0.1
app build.gradle 配置文件:apply plugin 根据 ISMAINAPP 值设置为 application 或 library
if (IS_MAIN_APP.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
android {
compileSdkVersion Integer.parseInt(COMPILE_SDK_VERSION)
buildToolsVersion BUILDTOOLS_VERSION
defaultConfig {
if (IS_MAIN_APP.toBoolean()) {
applicationId "com.fenlibao.arouter"
}
minSdkVersion Integer.parseInt(MIN_SDK_VERSION)
targetSdkVersion Integer.parseInt(TARGET_SDK_VERSION)
versionCode Integer.parseInt(APP_VERSION)
versionName APP_VERSION_CODE
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName(), AROUTER_GENERATE_DOC: "enable"]
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
if (IS_MAIN_APP.toBoolean()) {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
}
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
compile project(':common-module')//公共方法 基类 放在该项目中
if (IS_MAIN_APP.toBoolean()) {
compile project(':mall-module')//商城模块
compile project(':member-module')//会员模块
}
}
创建 mall-module: mall-module build.gradle 配置文件 apply plugin 根据 ISMALLAPP 值设置为 application 或 library
if (IS_MALL_APP.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
android {
compileSdkVersion Integer.parseInt(COMPILE_SDK_VERSION)
buildToolsVersion BUILDTOOLS_VERSION
defaultConfig {
if (IS_MALL_APP.toBoolean()) {
applicationId "com.fenlibao.mall"
}
minSdkVersion Integer.parseInt(MIN_SDK_VERSION)
targetSdkVersion Integer.parseInt(TARGET_SDK_VERSION)
versionCode Integer.parseInt(APP_VERSION)
versionName APP_VERSION_CODE
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
resourcePrefix "mall_"
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
sourceSets {
main {
if (IS_MALL_APP.toBoolean()) {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
}
}
}
}
dependencies {
compile project(':common-module')
compile 'com.android.support.constraint:constraint-layout:1.1.3'
annotationProcessor 'com.alibaba:arouter-compiler:1.2.2'
}
common-module 创建: common-module build.gradle 配置文件 apply plugin 固定为'com.android.library'作为库被其他项目引用
apply plugin: 'com.android.library'
android {
compileSdkVersion Integer.parseInt(COMPILE_SDK_VERSION)
buildToolsVersion BUILDTOOLS_VERSION
defaultConfig {
minSdkVersion Integer.parseInt(MIN_SDK_VERSION)
targetSdkVersion Integer.parseInt(TARGET_SDK_VERSION)
versionCode Integer.parseInt(APP_VERSION)
versionName APP_VERSION_CODE
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
javaCompileOptions {
annotationProcessorOptions {
arguments = [ AROUTER_MODULE_NAME : project.getName() ]
}
}
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
dependencies {
compile fileTree(dir: 'libs', include: ['*.jar'])
compile 'com.android.support:appcompat-v7:28.0.0'
compile 'com.android.support:design:28.0.0'
compile 'com.android.support.constraint:constraint-layout:1.1.3'
testCompile 'junit:junit:4.12'
androidTestCompile 'com.android.support.test:runner:1.0.2'
androidTestCompile('com.android.support.test.espresso:espresso-core:3.0.2', {
exclude group: 'com.android.support', module: 'support-annotations'
})
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
//该项目中suppourt包为25.2.0版本比较低 顾做排除处理
compile('com.alibaba:arouter-api:1.4.1') {
exclude group: 'com.android.support'
}
//Rxjava2
compile 'io.reactivex.rxjava2:rxjava:2.2.6'
compile 'io.reactivex.rxjava2:rxandroid:2.1.0'
//Retrofit2
compile 'com.squareup.retrofit2:retrofit:2.4.0'
compile 'com.squareup.retrofit2:converter-gson:2.4.0'
compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'
//Okhttp-interceptor
compile 'com.squareup.okhttp3:logging-interceptor:3.6.0'
}
4.注意事项
多个 module 间 Activity 如何跳转?
由于模块与模块之间是相互独立存在,因而不能使用项目内方式实现 Activity 跳转,下面介绍两种 Activity 跳转方式,日常开发中推荐使用方式二,方式二不管如何修改类名包名都能保持与字符串常量映射关系,维护和使用更方便,更多 ARouter 高级用法请查阅相关资料。
通过反射方式:
try {
Class clazz = Class.forName("com.fenlibao.member.MainActivity");
Intent intent = new Intent(_activity, clazz);
startActivity(intent);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
使用 ARouter 注解方式,实现映射关系自动注册:
/**
* ARouter在MyApplication中初始化
*/
public class MyApplication extends BaseApplication {
@Override
public void onCreate() {
super.onCreate();
init();
}
private void init() {
if (isDebug()) {
ARouter.openLog();
ARouter.openDebug();
}
ARouter.init(this);
}
public boolean isDebug() {
return getApplicationInfo() != null && (getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
}
@Override
public void onTerminate() {
super.onTerminate();
ARouter.getInstance().destroy();
}
}
/**
* 在常量类中定义静态字符串常量
*/
public static final String MEMBER_1_URL_MAIN = "/member/MainActivity";
/**
* 在对应module Activity中使用@Route标签进行绑定
*/
@Route(path = RoutePath.MALL_1_URL_MAIN)
public class MainActivity extends BaseActivity {
}
/**
* 在需要跳转至该页面build方法中传入对应Activity字符串常量,即可实现跳转
*/
ARouter.getInstance().build(RoutePath.MEMBER_1_URL_MAIN).navigation();
在 module 中如何获取 Application 上下文对象?
在 common-module 中定义 BaseApplication 继承 Application。
**
* 项目Application基类主项Application需集成此类
*/
public class BaseApplication extends Application {
/**
* 系统上下文
*/
private static BaseApplication mAppContext;
@Override
public void onCreate() {
super.onCreate();
mAppContext = this;
}
/**
* 获取系统上下文单例
*/
public static BaseApplication getInstance() {
return mAppContext;
}
}
在 app 项目中 MyApplication(上部分代码示例中可以查看)继承 BaseApplication,同时设置到 AndroidManifest.xml application 标签下 name 属性中。
在 app 及 module 中都可通过 BaseApplication.getInstance();方法即可获取应用上下文对象,如下:
BaseApplication application = BaseApplication.getInstance();
如何将 module 作为应用单独运行?
在项目 gradle.properties 全局配置文件中找到 ISMAINAPP 配置变量,设置为 ture 时为主项目启动方式,设置为 false,脱离其他 mall-module 作为应用单独启动,设置完成之后重新编译项目即可,开发测试阶段推荐使用该方式,可缩短项目编译时间,项目越大,缩短时间越明显。
##IS_MAIN_APP true主项目为应用,mall-module为library;false mall-module为应用可单独启动
IS_MAIN_APP=true
应用主入口只能有一个,如何切换应用主入口?
根据 ISMAINAPP 设置值加载不同目录下的 AndroidManifest.xml 文件,实现根据参数加载配置文件,下面是 app mall-module 的 build.gradle 配置。
#app build.gradle
android {
sourceSets {
main {
if (IS_MAIN_APP.toBoolean()) {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
}
}
}
}
#mall-module build.gradle
android {
sourceSets {
main {
if (!IS_MAIN_APP.toBoolean()) {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
}
}
}
}
如何避免资源文件冲突覆盖?
在组件化开发过程中经常会出现资源文件冲突覆盖问题,主项目会覆盖 module 项目资源文件。
在 module 项目中 build.gradle 配置文件中设置:
resourcePrefix "mall_"
通过给模块设置不同的前缀,避免资源文件重名覆盖问题。
良好的命名(资源文件/类文件)习惯,需以 module 前缀开头,例如 mall-module 布局资源文件,采用 mallactivity_home,方式避免资源文件重名覆盖问题,如果资源文件不按 resourcePrefix "mall" 定义前缀命名会有错误提示,例如以如下方式命名布局资源文件 activitymallhome.xml:
组件化开发如何利用 Git 管理代码,实现协同开发?
项目中引用的 module 可能是其他组员负责开发,我们只负责调用传递数据,当 module 发生改变时,项目中需要可以获取到最新的代码,本项目中采用 git submodule 方式管理代码,如下图:
1. 增加 git module 项目引用 采用 git submodule add [url][module-name] 命令增加 git 项目引用 url:git 项目完整路径; module-name:为本地项目 module 名称
2. 导入完整 git submodule 项目 第一步初始化:git submodule init;第二步更新项目:git submodule update 引用至本地,如果遇到提示项目目录已存在,将原有项目目录删除再执行更新命令即可。
5.总结
以上就是组件化开发的详细步骤和相关注意事项,如果你面前是一个庞大的工程,建议你使用该方案,以最小的代价尽快开始实施组件化。如果你现在负责的是一个开发初期的项目,代码量还不大,那么也建议尽快进行组件化的规划,不要给未来的自己增加徒劳的工作量。 项目示例 Github 地址:https://github.com/guixia/Android-Submodule.git。
- END -
更多推荐内容
《kubernetes 单master集群的搭建》