[TOC]
组件化随着APP业务的不断增加,版本不断迭代,APP越来越臃肿,所带来的问题也随之增加。模块化架构主要思路就是“分而治之”,把依赖整理清楚,减少代码冗余和耦合。在把代码抽取到各自模块后,了解各个模块的通信方式,以及可能发生的问题,规避问题或者解决问题。
- 构建速度慢(Gradle 无规则依赖),开发效率低。
- 迭代速度慢,耦合严重,无法单独测试
- 代码冲突多,编译慢
- 加快编译速度,可以把不会经常变动的组件做成静态库,同时每个组件可以独立编译,不依赖于主工程或者其他组件
- 业务模块解耦,模块、组件之间“各司其职”,加快业务迭代速度
- 测试困扰,模块之间可以独立运行、测试
- 多条业务线可以并行开发,提高开发效率
- 控制代码权限,将代码的权限细分到更小的粒度
- 公共模块采用依赖库方式,提供给各个业务线使用,减少重复开发和维护工作量
模块化组件化,其中的界限是什么,模块化和组件化的本质区别又是什么?为了解决这些问题,我们就要先了解 “模块” 和 “组件” 的区别。
[模块] 和 [组件] 间最明显的区别就是模块相对与组件来说粒度更大,一个模块中可能包含多个组件。并且两种方式的本质思想是一样的,都是为了代码重用和业务解耦。在划分的时候,模块化是业务导向,组件化是功能导向。
▍组件
组件指的是单一的功能组件,如 [视频组件]、[直播组件] 等,每个组件都可以以一个单独的 module 开发,并且可以单独抽出来作为 SDK 对外发布使用
▍模块
模块化就是将一个程序按照其功能做拆分,分成相互独立的模块,以便于每个模块只包含与其功能相关的内容,模块我们相对熟悉,比如登录功能可以是一个模块,搜索功能可以是一个模块等等
config.gradle主要来管理统一的SDK版本,避免版本冲突
ext {
isApplication = false //false:作为Lib组件存在, true:作为application存在,这个不建议改
isLoginApplication = true //登录模块开关,false:作为Lib组件存在, true:作为application存在
isLiveApplication = true //直播模块开关,false:作为Lib组件存在, true:作为application存在
android = [
compileSdkVersion : 28,
buildToolsVersion : "28.0.3",
minSdkVersion : 17,
targetSdkVersion : 28,
versionCode : 22,
versionName : "1.8.2" //必须是int或者float,否则影响线上升级
]
version = [
androidSupportSdkVersion: "28.0.0",
retrofitSdkVersion : "2.4.0",
glideSdkVersion : "4.8.0",
canarySdkVersion : "1.5.4",
constraintVersion : "1.0.2",
okhttp : "3.8.1"
]
dependencies = [
//support
"appcompat-v7" : "com.android.support:appcompat-v7:${version["androidSupportSdkVersion"]}",
//router
"router" : "com.alibaba:arouter-api:1.4.1",
"router-compiler" : "com.alibaba:arouter-compiler:1.2.2",
]
}
在 AndroidStudio 开发 Android 项目时,使用的是 Gradle 来构建,具体来说使用的是 Android Gradle 插件来构建,Android Gradle 中提供了两种种插件,在开发中可以通过配置不同的插件来配置不同的工程。
- App 插件,id: com.android.application
- Library 插件,id: com.android.libraay
区别比较简单, App 插件来配置一个 Android App 工程,项目构建后输出一个 APK 安装包,Library 插件来配置一个 Android Library 工程,构建后输出 aar 包。
通过在 module 中添加一个 gradle.properties 配置文件,在配置文件中添加一个布尔类型的变量 isAsAppRun,在 build.gradle 中通过 isAsAppRun 的值来使用不同的插件从而配置不同的工程类型,在组件调试和集成调试时直接修改 isAsAppRun 的值即可。例如,在 Login 登录组件中的配置:
▍gradle.properties 示例
# Project-wide Gradle settings.
# IDE (e.g. Android Studio) users:
# Gradle settings configured through the IDE *will override*
# any settings specified in this file.
# For more details on how to configure your build environment visit
# http://www.gradle.org/docs/current/userguide/build_environment.html
# Specifies the JVM arguments used for the daemon process.
# The setting is particularly useful for tweaking memory settings.
org.gradle.jvmargs=-Xmx1536m
# When configured, Gradle will run in incubating parallel mode.
# This option should only be used with decoupled projects. More details, visit
# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
# org.gradle.parallel=true
isAsAppRun=false
▍模块模式切换代码
if (isAsAppRun.toBoolean()) {
//组件模式
apply plugin: 'com.android.application'
} else {
// 集成模式
apply plugin: 'com.android.library'
}
一个 APP 是只有一个 ApplicationId 的,所以在单独调试和集成调试时组件的 ApplicationId 应该是不同的; 而组件模式又是可以单独打包,必然涉及到AndroidManifest 清单文件的合并的问题;所以除了通过依赖的插件来配置不同的工程,我们还要根据 isAsAppRun 的值来修改其他配置。
ApplicationId 和 AndroidManifest 文件都是可以在 build.gradle 文件中进行配置的,所以我们同样通过动态配置组件工程类型时定义的 isAsAppRun 这个变量的值来动态修改 ApplicationId 和 AndroidManifest。首先我们要新建一个 AndroidManifest.xml 文件,加上原有的 AndroidManifest 文件,在两个文件中就可以分别配置单独调试和集成调试时的不同的配置
▍清单文件配置
组件模式:src/main/manifest/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.loong.login">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".LoginActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
集成模式:src/main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.loong.login">
<application
android:label="@string/app_name"
android:theme="@style/AppTheme">
<activity android:name=".LoginActivity" />
</application>
</manifest>
然后在 build.gradle 中通过判断 isAsAppRun 的值,来配置不同的 ApplicationId 和 AndroidManifest.xml 文件的路径:
▍applicationId、清单文件动态配置
android {
compileSdkVersion compile_sdk_version.toInteger()
defaultConfig {
if (isAsAppRun.toBoolean()) {
applicationId "com.loong.login"
}
...
}
sourceSets {
main {
if (isAsAppRun.toBoolean()) {
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
...
}
在组件化开发的时候,组件之间是没有依赖关系,我们不能在使用显示调用来跳转页面了,因为我们组件化的目的之一就是解决模块间的强依赖问题,假如现在要从A业务组件跳转到业务B组件,并且要携带参数跳转,这时候就需要引入“路由”的概念了。 在介绍路由之前,我们先看下传统架构模式和组件化之后的架构模式有什么不同。
▍传统APP架构图
▍存在的问题
往往是在一个界面中存在大量的业务逻辑,而业务逻辑中充斥着各种网络请求、数据操作等行为,整个项目中也没有模块的概念,只有简单的以业务逻辑划分的文件夹,并且业务之间也是直接相互调用、高度耦合在一起的。
组件化工程模型下的业务关系,业务之间将不再直接引用和依赖,而是通过“路由”这样一个中转站间接产生联系。在这个演示项目中,使用的阿里开源的路由框架。
▍示意图
▍项目配置
componentbase 模块下的build.gradle 配置 Base 模块下build.gradle 文件中配置路由
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
api(rootProject.ext.dependencies["appcompat-v7"]) {
exclude module: "support-v4"
exclude module: "support-annotations"
}
// 配置路由
api 'com.alibaba:arouter-api:1.3.1'
}
其余模块下gradle 配置
defaultConfig {
....
javaCompileOptions {
annotationProcessorOptions {
arguments = [ moduleName : project.getName() ]
}
}
}
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation project (':base')
//// arouter-compiler 的注解依赖需要所有使用 ARouter 的 model 都添加依赖
annotationProcessor 'com.alibaba:arouter-compiler:1.1.4'
}
▍使用方式
ARouter统一入口封装工具类
/**
* 简单的跳转页面
* @param string string目标界面对应的路径
* @param callback 监听路由过程
*/
public static void navigation(String string , Context context , NavigationCallback callback){
if (string==null){
return;
}
ARouter.getInstance()
.build(string)
.navigation(context,callback);
}
// 监听
public static NavigationCallback getCallback(){
NavigationCallback callback = new NavCallback() {
@Override
public void onArrival(Postcard postcard) {
Log.i(TAG,"ARouterUtils"+"---跳转完了");
}
@Override
public void onFound(Postcard postcard) {
super.onFound(postcard);
Log.i(TAG,"ARouterUtils"+"---找到了");
}
@Override
public void onInterrupt(Postcard postcard) {
super.onInterrupt(postcard);
Log.i(TAG,"ARouterUtils"+"---被拦截了");
}
@Override
public void onLost(Postcard postcard) {
super.onLost(postcard);
Log.i(TAG,"ARouterUtils"+"---找不到了");
//无法找到路径,作替换处理
PathReplaceServiceImpl pathReplaceService = new PathReplaceServiceImpl();
// pathReplaceService.replacePath(ARouterConstant.ACTIVITY_ANDROID_ACTIVITY,ARouterConstant.ACTIVITY_DOU_MUSIC_ACTIVITY);
}
};
return callback;
}
定义路由跳转路径
public class ARouterConstant {
//跳转到登录页面
public static final String ACTIVITY_LOGIN = "/account/login/LoginActivity";
//跳转到直播页面
public static final String ACTIVITY_LIVE = "/share/share/LiveActivity";
public static final String ACTIVITY_ANDROID_ACTIVITY = "/degrade/web/DegradeWebActivity";
}
调转LoginActivity 方式
/* 跳登录界面
*
* @param view
*/
public void login(View view) {
ARouterUtils.navigation(ARouterConstant.ACTIVITY_LOGIN,this,ARouterUtils.getCallback());
}
目标Activity 定义@Router
@Route(path = ARouterConstant.ACTIVITY_LOGIN)
public class LoginActivity extends AppCompatActivity {
// 代码省略
}
使用该方式即可实现组件之间的通信。以及数据传递。该文档不再过多的介绍ARouter原理以及执行流程。具体相关的ARouter 分析。详见下一篇ARouter流程分析
除了 Activity 的跳转,我们在开发过程中也会经常使用 Fragment,一种很常见的样式就是应用主页 Activity 中包含了多个隶属不同组件的 Fragment。一般情况下,我们都是直接通过访问具体 Fragment 类的方式实现 Fragment 的实例化,但是现在为了实现模块与组件间的解耦,在移除组件时不会由于引用的 Fragment 不存在而编译失败,我们就不能模块中直接访问组件的 Fragment 类。
▍解决方案:
① 通过反射来解决,通过来初始化 Fragment 对象并返回给 Activity,在 Actiivty 中将 Fragment 添加到特定位置即可。
② 通过ARouter 路由方式获取
/**
*
* @param path path目标界面对应的路径
* @return
*/
public static Fragment getTargetFragment(String path){
Fragment fragment;
if (TextUtils.isEmpty(path)){
return null;
}
fragment = (Fragment) ARouter.getInstance()
.build(path)
.navigation();
return fragment;
}