组件化基础
文章目录
一、什么是组件化?
在项目开发中,一般将公共的代码提取出来制作基础组件库Base module,将某些单独的功能封装到Libaray module中,更具业务划分module。
但是随着项目更迭,功能越来越多代码结构越来越复杂。
这时候就出现了组件化
、模块化
、插件化
这些规则。
多module划分业务和基础功能,这个概念将作为组件化的基础
**组件:**指的是单一的功能组件,如:视频组件(VideoSDK)、支付组件(PaySdk)
模块 : 指的是独立的业务模块,如直播模块(LiveModule)
二、组件化架构介绍
- 基础层包含一些基础库和对基础库的封装,包括图片加载、网络加载、数据存储等
- 组件层包含简单的业务、比如登录注册、支付、直播等等
- 应用层用于统筹全部组件
聚合和解耦是项目架构的基础
三、组件化项目的搭建
1、了解组件化项目框架
在这里介绍一下项目搭建的两种插件:
- application插件:如果一个模块被声明为aoolication,那么它会成为一个apk文件,是可以直接安装运行的项目。
- library插件:被声明为library的模块,它会成为一个aar文件,不可以单独运行。
下面是一个构建的模块,可以先了解一下大致框架,后面会一步步介绍。
这里创建了两个包,一个是modulesBase
这里保存了基础的公用代码和依赖。另一个是modulesCore
这个写的是主要的业务模块。
2、抽离共用的build.gradle版本数据
我们在APP开发过程中肯定会涉及到不同的手机版本信息,系统信息等等。如果每个模块的信息都是独立的,那么一旦出现版本问题需要修改的情况,将会变得非常麻烦。
这时就需要使用config.gradle
统一管理项目的依赖库
2.1 项目下新建一个Gradle文件
用于存储项目基本信息,在这里面,我们写上所有依赖库,以及项目中sdk等等的版本号,然后直接让buil.gradle去apply它,之后有什么版本的修改升级就可以直接在这里改。
2.2 定义一个ext扩展区域
用于定义额外的属性和变量,以便在整个构建脚本中共享和重复使用。
//在 Gradle 构建脚本中定义了一些 Android 项目的配置和依赖项版本信息。
ext {
isDebug = false; //当为true时代表调试模式,组件可以单独运行。如果是false代表编译打包
/**
compileSdkVersion:指定用于编译应用程序的 Android SDK 版本。
minSdkVersion:指定应用程序可以运行的最低 Android 版本。
targetSdkVersion:指定应用程序目标的 Android 版本。
versionCode:用于标识应用程序版本的整数值。
versionName:用于标识应用程序版本的字符串值。
*/
android = [
compileSdkVersion: 33,
minSdkVersion : 32,
targetSdkVersion : 33,
versionCode : 1,
versionName : "1.0"
]
/**
* 这是每个模块的application地址
*/
applicationId = [
"app" : "com.example.dome",
"live" : "com.example.module.live",
"net" : "com.example.module.net",
"login": "com.example.module.login",
"router": "com.example.module.librouter"
]
//定义了一些常见的 Android 库的依赖项,包括 AppCompat 库、Material Design 库和 ConstraintLayout 库。
library = [
"appcompat" : "androidx.appcompat:appcompat:1.6.1",
"material" : "com.google.android.material:material:1.5.0",
"constraintlayout": "androidx.constraintlayout:constraintlayout:2.1.4"
]
//第三方库单独放置
libGson = "com.google.code.gson:gson:2.8.6"
libARouter = "com.alibaba:arouter-api:1.5.2"
libARouterCompiler = "com.alibaba:arouter-compiler:1.5.2"
}
相信大家对这些东西都不陌生了,第三方库后面会说到怎么用,这里可以先写出来。
2.3 在build.gradle中导入config.gradle
3、创建基础层
3.1 建立一个libBase模块
就是这里的modulesBase模块,接下来我们尝试创建一个libBase。
注意:
在这个地方我们一般有两个选项
第一个选项是创建可以单独运行的模块,也就是业务模块,是一个application。
第二个是创建一个Library,一般是用于创建基础层,不需要单独运行。
在这里我们创建Module name时多加了一层,这样可以用一个专门的包去管理所有的模块。
就是这样的效果:
创建Package name时,也多加了一个module,这个也是为了放在冲突,因为默认的这种创建方式的包名和主App的命名方式一致,导包的过程就会出现冲突。当然这个module可以根据自己的情况命名。
3.2 修改libbase的builde.gradle
我们创建的library模块会自动设置启动模式为apply plugin: 'com.android.library'
,这样基础模块就会被打包成一个arr文件,配合其他的业务模块application使用。
apply plugin: 'com.android.library'
//简化手写,rootProject.ext的次数,提高性能
def cfg = rootProject.ext
android {
//命名空间
namespace cfg.applicationId.net
compileSdkVersion cfg.android.compileSdkVersion
defaultConfig {
//版本号
minSdkVersion cfg.android.minSdkVersion
targetSdkVersion cfg.android.targetSdkVersion
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
api cfg.library.appcompat
api cfg.library.material
api cfg.library.constraintlayout
api cfg.libGson
//也可以使用这样的方式一行代码完成:,相当于一个for循环遍历library
library.each{
k,v->implementation v}
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
在 Gradle 构建中,rootProject.ext是指向项目根目录的引用,在dependencies引入在最外部定义的公用依赖。
这里采用Api的方式引用,这样做的好处是依赖可以传递。
比如A模块使用api导入了x依赖,B模块此时只需要导入A模块就可以使用x依赖。
4、创建组件层
4.1 建立一个login组件模块
这里的和刚才libbase是一样的命名思想。
4.2 修改login的builde.gradle
首先我们将login的静态启动模式修改为动态启动模式
plugins {
id 'com.android.application'
}
还记得我们在ext定义的isDebug吗?在这里我们就需要用到它了。
修改为:
//判断属于打包模式还是调试模式
if (rootProject.ext.isDebug) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
这样做的好处是在调试的模式下就是单独的application进行运行,在打包的模式就会变成一个arr文件,作为组件配合主App使用。
接下来还是同样的操作,将涉及到共用部分的写成ext的形式。
//判断属于打包模式还是调试模式
if (rootProject.ext.isDebug) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
def cfg = rootProject.ext
android {
namespace cfg.applicationId.login
compileSdkVersion cfg.android.compileSdkVersion
defaultConfig {
//判断如果是调试模式则需要当做一个application启动,则需要一个y
if (cfg.isDebug) {
applicationId cfg.applicationId.login
}
minSdkVersion cfg.android.minSdkVersion
targetSdkVersion cfg.android.targetSdkVersion
versionCode cfg.android.versionCode
versionName cfg.android.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
//选择合适的manifest
sourceSets {
main {
if (cfg.isDebug) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
//注意我们还需在正式环境下屏蔽Debug包下的所有文件
java{
exclude "**/debug/**"
}
}
}
}
}
dependencies {
//导入基础模块
implementation project(':modulesBase:libBase')
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
接下来是详细解释:
在defaultConfig
中判断是否需要应用标识符,如果是调试模式则需要一个标识符启动这个单独的login模块。
if (cfg.isDebug) {
applicationId cfg.applicationId.login
}
接下来在android
中添加sourceSets
用于指定Android项目的源集配置,这特定的配置块用于指定在构建 Android 应用时要使用的源代码、资源和清单文件的位置。
在debug模式下需要有详细的AndroidManifest去启动一个活动,新建一个debug包,我们将自动生产的AndroidManifest拷贝到debug包下
但是在打包模式下,就不需要其他的的活动清单了,我们将原来自动生成的AndroidManifest修改为以下样式:
sourceSets
定义了一个main
源集。- 对于
main
源集,使用了一个条件语句(if (cfg.isDebug)
)来决定使用哪个清单文件。"**/debug/**"
: 这是一个 Ant 风格的通配符。**
表示匹配任意数量的目录,因此**/debug/**
表示匹配任何包含 “debug” 目录的路径。
sourceSets {
main {
if (cfg.isDebug) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
//注意我们还需在正式环境下屏蔽Debug包下的所有文件
java{
exclude "**/debug/**"
}
}
}
}
然后在dependencies
中导入了刚才的libBase模块,此时login就具有了libBase所存在的依赖。
implementation project(':modulesBase:libBase')
5、搭建App层
5.1 修改App的build.gradle
基本和刚才login组件的修改方法相似,plugins不用改动,因为App层不会拿来当作library。
plugins {
id 'com.android.application'
}
需要注意的就是dependencies
。同样的app也需要导入基础模块的依赖,并且app是关联其他的多个组件,这里还需要导入其他组件例如:login
因此在这里也需要判断是否是debug模式,如果是debug模式就不需要导入这些组件。
implementation project(':modulesBase:libBase')
//如果不是Debug的情况下则导入各个组件的模块
if (!cfg.isDebug){
implementation project(":modulesCore:live")
implementation project(":modulesCore:login")
}
完成的修改格式如下:
plugins {
id 'com.android.application'
}
def cfg = rootProject.ext
android {
namespace cfg.applicationId.app
compileSdkVersion cfg.android.compileSdkVersion
defaultConfig {
applicationId cfg.applicationId.app
minSdkVersion cfg.android.minSdkVersion
targetSdkVersion cfg.android.targetSdkVersion
versionCode cfg.android.versionCode
versionName cfg.android.versionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
//让java代码也可以使用isDebug
buildConfigField("boolean","isDebug",String.valueOf(isDebug))
}
//启用buildConfigField功能
buildFeatures {
buildConfig true
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation project(':modulesBase:libBase')
//如果不是Debug的情况下则导入各个组件的模块
if (!cfg.isDebug){
implementation project(":modulesCore:live")
implementation project(":modulesCore:login")
}
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
}
5.2 将Gradle的数据导入到java中
在这里介绍一下buildConfigField
,他可以将gradle的数据映射到java类里面,实现在java代码中使用gradle数据。
注意是项目自带的BuildConfig而不是Arouter的BuildConfig。
6、检验搭建成果
当ext下的isDebug为true时,刚才的app和login组件都可以独立运行。
当ext下的isDebug为false时,只有app可以运行。login被打包成arr文件。
四、使用路由(Router)跳转不同组件的Activity
在我们没有应用组件化开发之前,界面跳转我们主要用显式Intent和隐式Intent两种方法。但是组件化开发中,是不允许组件层的模块横向依赖的,所以不可以直接访问彼此的类,就不能用显式Intent来跳转,此时我们就需要使用路由的方式跳转。
1、自定义路由
这里主要是自己尝试手写一个简单的路由,便于后面更好的理解路由工作机制
我们首先右键点击刚才的modulesBase包,然后在这个包下创建一个libRouter模块
创建方式还是和刚才一样,我们此时就得到了一个libRouter模块。
接下来我们新建立一个java文件,实现路由功能。
1.1 设计模式的选择
很明显这个Router一定是一个单例模式,这样才能保证每次共用的是同一个对象共享相同的数据。
private final static class Holder{
static Router INSTANCE = new Router();
}
public static Router getInstance(){
return Holder.INSTANCE;
}
1.2 使用什么结构保存不同的Class
Router的作用就是管理不同的类,可以统一协调和使用这些类,那么我们自然就需要一个存储这些类的方式。
显然这些类和他们的包名,路径存在对应关系,我们可以通过这些包名找到这些类。那么就需要一个Map了。
Map的结构模式如下:
static Map<String,Map<String,Class<?>>> groupMap = new HashMap<>();
Map<String,Class<?>> routerMap = new HashMap<>();
1.3 具体如何实现保存不同的Class
首先我们思考如果设计一个注册这些类的方法,那么应该需要什么样的参数?
回到刚才的存储结构当中,可以看出我们需要两个key,一个GroupName和一个Path,一个Path当中肯定包含一个GroupName。那么这个注册方法就需要一个Path,既然保存Class当然还需要一个Class.
public void register(String path,Class<?> cla)
函数参数就出来了,接下来是分析函数结构:
1、既然是Path中包含GroupName,那么我们需要解析Path,我们从文件的角度很容易知道Path是这样的一个字符串:
/main/MainActivity
我们需要将main解析出来,然后创建一个保存Class结构即可。
String[] strArray = path.split("/");
//解析结果如下:
"" "main" "MainActivity"
2、实现注册的逻辑如下:
/**
* @param path /main/MainActivity
* @param cla 需要存储的class
*/
public void register(String path, Class<?> cla) {
String[] strArray = path.split("/");
if (strArray.length > 2) {
String groupName = strArray[1];
Map<String, Class<?>> routerMap = null;
if (groupMap.containsKey(groupName)) {
routerMap = groupMap.get(groupName);
} else {
routerMap = new HashMap<>();
groupMap.put(groupName, routerMap);
}
routerMap.put(path, cla);
}
}
1.4 如何使用路由跳转到不同的Activity
还是同样的分析函数参数,实现不同Activit的跳转,我们需要知道当前的Activit和需要跳转的Activity。
而不同的Activit实际上我们刚才已经放入groupMap
当中了,我们只需要知道一个Path就可以取出这个Class
此时函数的参数就有了:
public void startActivity(Activity activity, String path)
代码逻辑也很简单两个if语句判断一下就行了
/**
* @param activity 当前的Activity
* @param path 需要启动的路径
*/
public void startActivity(Activity activity, String path) {
String[] strArray = path.split("/");
if (strArray.length > 2) {
String groupName = strArray[1];
String routeName = path;
Map<String, Class<?>> group = null;
if (groupMap.containsKey(groupName)) {
group = groupMap.get(groupName);
if (group != null && group.containsKey(routeName)) {
Class<?> clz = group.get(routeName);
activity.startActivity(new Intent(activity, clz));
}
}
}
}
1.5 体验自定义路由
在modulesCore模块下新建一个live业务,方法和上述一样。
然后我们进入App中,新建一个MyApplication继承Application.在onCreate这里注册路由表
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
Router.getInstance().register("/live/LiveActivity", LiveActivity.class);
Router.getInstance().register("/login/LoginActivity", LoginActivity.class);
}
}
然后我们进入login业务中,设置一个点击事件,用于跳转到live业务。
public class LoginActivity extends AppCompatActivity {
private Activity activity;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
activity = this;
TextView view = findViewById((int) R.id.logView);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Router.getInstance().startActivity(activity,"/live/LiveActivity");
}
});
}
}
在app下的MainActivity设置启动login模块,然后将ext下的isDebug设置为true即可。
==注意:==在这里如果你的app、live、login这几个模块下的layout布局的名称都是activity_main.xml就会出现资源冲突的问题,所以需要改一下不同的名字。
2、使用第三方路由插件——ARouter
github仓库地址:ARouter
当前版本是:
2.1导入依赖的方法
如下:
需要在哪个业务下使用Arouter就需要在该组件的build.gradle下配置Arouter注解器。也就是1和2
ARouter的基本依赖可以通过Api放在基础模块libRouter中
然后在libBase下导入libRouter即可:
导入依赖后运行一下看看会不会报错,如果出现类似于这样的错误:
1: Task failed with an exception.
What went wrong:
Execution failed for task ‘:modulesCore:login:processDebugMainManifest’.Manifest merger failed with multiple errors, see logs
Try:
Run with --info or --debug option to get more log output.
Run with --scan to get full insights.
可能是Arouter没有引入AndroidX库,需要转化为Androidx库支持的模式
在最外面的gradle.properties下加入这段代码:
android.useAndroidX=true
android.enableJetifier=true
这样就可以自动转为Androidx库支持的模式,具体参考谷歌官方文档:迁移至 AndroidX
2.2 基本使用方法——实现不同组件的跳转
首先给login业务和live业务和app都加上这个注解处理器的依赖修改build.gradle。
和上面自定义路由的使用方法一样,我们首先肯定要保存这些Class,然后才能使用这些Class。
这里我们需要使用Arouter的注解器将每个Class加入进来。
第一个路径是包名,第二个路径是类名和刚才自定义路由的用法一样。
接下来还是在MyApplication下进行初始化,BuildConfig.DEBUG
可以获取当前的模式
然后在刚刚所有使用自定义路由跳转的方法改为Arouter即可
这样就可以实现跳转了,如果无法正常跳转可能是gradle版本过高只需要, 把这个if语句去掉重新加载一下即可
2.3 实现不同组件数据的传递
1、在libBase中创建一个类用于传递
2、在MainActivity中发送数据
3、在LoginActivity中接受数据
@Route(path = "/login/LoginActivity")
public class LoginActivity extends AppCompatActivity {
private Activity activity;
@Autowired
public String key1;
@Autowired(name = "key2")
public int str;
@Autowired
public Test key3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
activity = this;
TextView view = findViewById((int) R.id.logView);
view.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
// Router.getInstance().startActivity(activity,"/live/LiveActivity");
ARouter.getInstance().build("/live/LiveActivity").navigation();
}
});
ARouter.getInstance().inject(this);
Log.e("TAG", "onCreate: key1"+key1);
Log.e("TAG", "onCreate: key2"+str);
Log.e("TAG", "onCreate: key3"+key3.getName());
}
}
更多用法参考:Arouter开发教程