1.什么是组件化?
1.1 为什么要用组件化
在项目的开发过程中,随着开发人员的增多及功能的增加,如果提前没有使用合理的开发架构,那么代码会越来臃肿,功能间代码耦合也会越来越严重,这时候为了保证项目代码的质量,我们就必须进行重构
1.2 组件化的介绍
组件化是指解耦复杂系统时将多个功能模块拆分,重组的过程。在Android工程上表现上就是把app按照其业务的不同,划分为不同的Module
组件化和单一模块项目结构对比
![]() |
![]() |
1.3 组件化的优点
- 编译速度 :我们可以按需测试单一模块极大的提升了我们的开发速度
- 超级解耦 :极度的降低了模块之间的耦合,便于后期维护与更新
- 功能重用 : 某一块的功能在另外的组件化项目中使用只需要单独依赖这一模块即可
- 便于团队开发 : 组件化架构是团队开发必然会选择的一种开发方式,它能有效的使团队更好的协作
1.4 组件化的框架
先看一下整体的结构
举个例子:以某个直播平台为例(没有画完整),
- 基础层:包含的是一些基础库以及对基础库的封装,比如常用的图片加载,网络请求,数据存储操作等等,其他模块或者组件都可以引用同一套基础库,这样不但只需要开发一套代码,还解耦了基础功能和业务功能的耦合,在基础库变更时更加容易操作。
- 功能组件层:包含一些简单的功能组件,比如视频,支付等等
- 业务组件层:这是通过模块化划分出来的,即根据业务的不同划分为不同的模块,一个具体的业务模块会按需引用不同的组件,最终实现业务功能,如上有三个业务组件
- app层:多个业务模块,各自按需引用组件,最后将各个模块统筹输出 APP。
2.组件化框架的搭建
这里以一个业务组件层为例(首页home)进行组件化的搭建
新建一个android项目
2.1 Gradle中版本号的统一管理
在主模块下创建config.gradle文件夹(可以不是config但后缀必须是gradle),依赖库,项目中sdk等等的版本号把有重用的地方都放在这里,以后的每个模块的版本号都要引用这里的
ext{
android = [
compileSdkVersion : 30,
buildToolsVersion : "30.0.2",
minSdkVersion : 16,
targetSdkVersion : 30,
versionCode : 1,
versionName : "1.0"
]
dependencies = [
"appcompat" : 'androidx.appcompat:appcompat:1.2.0',
"material" : 'com.google.android.material:material:1.2.1',
"constraintLayout" : 'androidx.constraintlayout:constraintlayout:2.0.4',//约束性布局
//test
"junit" : "junit:junit:4.13.1",
"testExtJunit" : 'androidx.test.ext:junit:1.1.2',//测试依赖,新建项目时会默认添加,一般不建议添加
"espressoCore" : 'androidx.test.espresso:espresso-core:3.3.0',//测试依赖,新建项目时会默认添加,一般不建议添加
]
}
在主模块的build.gradle中加载config.gradle
//加载config.gradle
apply from: "config.gradle"
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath "com.android.tools.build:gradle:4.1.0"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
}
}
2.2 基础层的搭建
点击file->new->new module,选择library module模块。我这里的名字是commlib
打开此模块下的build.gradle,修改android和dependencies如下
android {
compileSdkVersion rootProject.ext.android["compileSdkVersion"]
buildToolsVersion rootProject.ext.android["buildToolsVersion"]
defaultConfig {
//applicationId "com.example.moduletextt" commlib是library,不需要applicationId
minSdkVersion rootProject.ext.android["minSdkVersion"]
targetSdkVersion rootProject.ext.android["targetSdkVersion"]
versionCode rootProject.ext.android["versionCode"]
versionName rootProject.ext.android["versionName"]
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
}
................................
}
dependencies {
api rootProject.ext.dependencies["appcompat"]
api rootProject.ext.dependencies["material"]
api rootProject.ext.dependencies["constraintLayout"]
testImplementation rootProject.ext.dependencies["junit"]
androidTestImplementation rootProject.ext.dependencies["testExtJunit"]
androidTestImplementation rootProject.ext.dependencies["espressoCore"]
}
让其它模块依赖与基础模块
在其他模块的build.gradle的dependencies中添加
implementation project(':commlib')
2.3功能层的搭建
2.3.1 创建一个module
new->module->phone&Tablet Module
这里在填写Library name 和 Activity name时注意包名和资源文件名命名冲突问题,避免和别的组件模块发生冲突。
2.3.2 统一版本号处理
修改build.gradle,注意要继承自基础层,继承基础层后基础层api对应的依赖不用重复去写,只写本模块需要的依赖
android {
compileSdkVersion rootProject.ext.android["compileSdkVersion"]
buildToolsVersion rootProject.ext.android["buildToolsVersion"]
defaultConfig {
applicationId "com.example.moduletextt"
minSdkVersion rootProject.ext.android["minSdkVersion"]
targetSdkVersion rootProject.ext.android["targetSdkVersion"]
versionCode rootProject.ext.android["versionCode"]
versionName rootProject.ext.android["versionName"]
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
............................
}
dependencies {
implementation project(':commlib')
// 在下面添加只有APP需要的依赖
testImplementation rootProject.ext.dependencies["junit"]
androidTestImplementation rootProject.ext.dependencies["testExtJunit"]
androidTestImplementation rootProject.ext.dependencies["espressoCore"]
}
2.3.3 组件在Application和Library之间做到随意切换
新建gradle.properties文件夹,添加
# true时为组件化模式开发,false时为集成模式开发
isRunAlone=true
修改build.gradle最上面plugins如下
if (isRunAlone.toBoolean()) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
2.3.4 AndroidManifest.xml文件的区分
在main目录下新建manifest文件夹,在该文件夹下新建AndroidManifest.xml,作为单独调试的AndroidManifest.xml
main/manifest/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.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/Theme.ModuleTextt">
<activity android:name=".LoginMainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>
main目录下的AndroidManifest.xml作为集成调试
main/AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.login">
<application
android:allowBackup="false">
<activity android:name=".LoginMainActivity">
</activity>
</application>
</manifest>
然后在 build.gradle 中通过判断 isRunAlone 的值,来配置不同的 ApplicationId 和 AndroidManifest.xml 文件的路径
android {
............
//sourceSets与defaultConfig平级
sourceSets {
main {
// 单独调试与集成调试时使用不同的 AndroidManifest.xml 文件
if (isRunAlone.toBoolean()) {
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
}
2.3.5 Library不能在Gradle文件中有applicationld
修改build.gradle->android->defaultConfig,添加判断条件
defaultConfig {
if (isRunAlone.toBoolean()) {
// 单独调试时添加 applicationId ,集成调试时移除
applicationId "com.example.login"
}
minSdkVersion rootProject.ext.android["minSdkVersion"]
targetSdkVersion rootProject.ext.android["targetSdkVersion"]
versionCode rootProject.ext.android["versionCode"]
versionName rootProject.ext.android["versionName"]
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
2.4 搭建业务组件层(即这里的home层)
创建一个业务层module ,统一版本号
和2.3.1 ,2.3.2完全相同
业务层的单独调试与集成调试
这里的处理和功能组件层的处理基本类似,这里介绍另一种调试方法:在主项目的config.gradle中添加布尔变量用于调试,每个业务层都用这个变量进行调试,这样做集成时不用一个一个去修改每个组件的gradle.properties,只需要修改这一处即可
ext{
android = [
compileSdkVersion : 30,
buildToolsVersion : "30.0.2",
minSdkVersion : 16,
targetSdkVersion : 30,
versionCode : 1,
versionName : "1.0",
//is_application判断业务组件层是否是单独调试
is_application : true
]
build.gradle文件(别忘了添加manifest/AndroidManifest.xml文件)
if (rootProject.ext.android["is_application"]) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
android {
..............................
sourceSets {
main {
// 单独调试与集成调试时使用不同的 AndroidManifest.xml 文件
if (rootProject.ext.android["is_application"]) {
manifest.srcFile 'src/main/manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
}
}
}
}
是否添加功能模块
这里用isRunAlone的值模拟判断依赖的功能模块是不是library
在home层下新建gradle.properties文件夹,用于是否添加功能模块
# true时为组件化模式开发,false时为集成模式开发
isRunAlone=true
修改build.gradle
dependencies {
implementation project(':commlib')
// 在下面添加只有APP需要的依赖
testImplementation rootProject.ext.dependencies["junit"]
androidTestImplementation rootProject.ext.dependencies["testExtJunit"]
androidTestImplementation rootProject.ext.dependencies["espressoCore"]
// 每加入一个新的模块,就需要在下面对应的添加一行
if (!isRunAlone.toBoolean()) {
implementation project(':live')
implementation project(':login')
implementation project(':pay')
}
}
2.5 app层
统筹业务组件层。写法和业务组件层类似,用 is_application 去判断是否要添加业务层的依赖,
if (!rootProject.ext.android["is_application"]) {
implementation project(':home')
implementation project(':my')
}
3.组件间数据传递与方法的相互调用
由于主项目与组件,组件与组件之间都是不可以直接使用类的相互引用来进行数据传递的,那么在开发过程中如果有组件间的数据传递时应该如何解决呢,这里我们可以采用 [接口 + 实现] 的方式来解决。
这里展示在home中得到login中传出的数据
3.1 基础层的配置
定义一个LoginService做传递方法的接口,LoginEmptyService类为接口的空实现,ServiceFactory接收组件中实现的接口对象的注册以及向外提供特定组件的接口实现。
代码结构:
public interface LoginService {
/*
* 是否已经登录
*/
boolean isLogin();
/*
* 获取登录用户的 AccountId
*/
String getAccountId();
}
public class LoginEmptyService implements LoginService {
@Override
public boolean isLogin() {
return false;
}
@Override
public String getAccountId() {
return null;
}
}
public class ServiceFactory {
private LoginService loginservice;
/**
* 禁止外部创建 ServiceFactory 对象
*/
private ServiceFactory() {
}
private static class Inner {
private static final ServiceFactory serviceFactory = new ServiceFactory();
}
/**
* 通过静态内部类方式实现 ServiceFactory 的单例
*/
public static ServiceFactory getInstance() {
return Inner.serviceFactory;
}
public LoginService getLoginservice() {
if(loginservice==null){
loginservice = new LoginEmptyService();
}
return loginservice;
}
public void setLoginservice(LoginService loginservice) {
this.loginservice = loginservice;
}
}
3.2 Login中的实现
- 创建类实现 LoginService 接口并实现其中的接口方法
public class LoginServiceClass implements LoginService {
@Override
public boolean isLogin() {
return true;
}
@Override
public String getAccountId() {
return "传递id";
}
}
- 将 接口实现类注册到ServiceFactory中
//Login组件的MainActivity
public class LoginMainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login_main);
ServiceFactory.getInstance().setLoginservice(new LoginServiceClass());
}
}
3.3 home组件中的调用
//调用isLogin()方法
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home_main);
IsLogin();
}
private void IsLogin() {
if(ServiceFactory.getInstance().getLoginservice().isLogin()){
Toast.makeText(this,"已经登录",Toast.LENGTH_LONG).show();
}else{
Toast.makeText(this,"没有登录",Toast.LENGTH_LONG).show();
}
}
4. 组件 Application 的动态配置
上面在把Login中接口实现类注册到ServiceFactory时我们是在LoginMainActivity中进行的,更好的做法是在Application中完成这些初始化操作。
为了解决这个问题可以将组件的 Service 类强引用到主 Module 的 Application 中进行初始化,这就必须要求主模块可以直接访问组件中的类。而我们又不想在开发过程中主模块能访问组件中的类,这里可以通过反射来实现组件 Application 的初始化。
- 在基础层定义抽象类继承自Application,里面定义了两个方法,initModeApp 是初始化当前组件时需要调用的方法,initModuleData 是所有组件的都初始化后再调用的方法。
public abstract class App extends Application {
/**
* Application 初始化
*/
public abstract void initModuleApp(Application application);
/**
* 所有 Application 初始化后的自定义操作
*/
public abstract void initModuleData(Application application);
}
- 所有的组件的 Application 都继承 App,并在对应的方法中实现操作
//以Login组件为例,把注册放在这里
public class LoginApp extends App {
@Override
public void onCreate() {
super.onCreate();
// 将 LoginServiceClass 类的实例注册到 ServiceFactory
initModuleApp(this);
initModuleData(this);
}
@Override
public void initModuleApp(Application application) {
ServiceFactory.getInstance().setLoginservice(new LoginServiceClass());
}
@Override
public void initModuleData(Application application) {
}
}
- 在基础层中定义 AppConfig 类,统一管理需要初始化的组件的 Application 。
public class AppConfig {
private static final String LoginApp = "com.example.login.LoginApp";
//定义一个数组吧需要初始化的组件的 Application的完整类名放在这里
public static String[] moduleApps = {
LoginApp
};
}
- 主Modle的Applicaiton配置
public class MainApp extends App {
@Override
public void onCreate() {
super.onCreate();
// 初始化组件 Application
initModuleApp(this);
// 其他操作
// 所有 Application 初始化后的操作
initModuleData(this);
}
@Override
public void initModuleApp(Application application) {
for(String moduleApp: AppConfig.moduleApps){
try{
Class clazz = Class.forName(moduleApp);
App app = (App)clazz.newInstance();
app.initModuleApp(this);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
@Override
public void initModuleData(Application application) {
for(String moduleApp: AppConfig.moduleApps){
try{
Class clazz = Class.forName(moduleApp);
App app = (App)clazz.newInstance();
app.initModuleData(this);
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
}
5. 组件间界面跳转
通过路由来实现页面跳转 GitHub地址
添加依赖和配置
- config.gradle
dependencies = [
..........
"ARouter" : "com.alibaba:arouter-api:1.5.1",//ARouter
"ARouterCompiler" : "com.alibaba:arouter-compiler:1.5.1",// ARouterCompiler
]
- 基础层的build.gradle的dependencies
// ARouter
api rootProject.ext.dependencies["ARouter"]
annotationProcessor rootProject.ext.dependencies["ARouterCompiler"]
- 需要跳转的组件层配置
//对应的build.gradle
android {
.......
javaCompileOptions {
annotationProcessorOptions {
arguments = [ AROUTER_MODULE_NAME : project.getName() ]
}
}
}
dependencies {
........
// ARouterCompiler
annotationProcessor rootProject.ext.dependencies["ARouterCompiler"]
}
初始化ARouter
//在主Moudle的Application中进行初始化操作
public abstract class MainApp extends App {
@Override
public void onCreate() {
super.onCreate();
// 初始化 ARouter
if (isDebug()) {
// 这两行必须写在init之前,否则这些配置在init过程中将无效
// 打印日志
ARouter.openLog();
// 开启调试模式(如果在InstantRun模式下运行,必须开启调试模式!线上版本需要关闭,否则有安全风险)
ARouter.openDebug();
}
// 初始化 ARouter
ARouter.init(this);
}
private boolean isDebug() {
return BuildConfig.DEBUG;
}
}
ARouter的使用
以从App层跳到home组件传递数据的为例
- home的homeMainActivity添加注解
@Route(path = "/account1/home")
public class homeMainActivity extends AppCompatActivity {
}
/*
注意:
1.路径需要注意的是至少需要有两级,/xx/xx
2.不可以出现路径相同的情况,不同的组件第一级目录不可以相同,同一组件的一级目录可以相同。
*/
- app组件的跳转逻辑
private void gotoLogin() {
ARouter.getInstance()
.build("/account1/home")
.withString("username","张三").navigation();
}
- home组件接收传递的参数
public class homeMainActivity extends AppCompatActivity {
@Autowired(name = "username")
String username = "";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_home_main);
ARouter.getInstance().inject(this); //注入(这句话不能少)
// ARouter会自动对字段进行赋值,无需主动获取
Log.d("MainActivity",username);
}