简介
模块:将一个程序按照其功能做拆分,分成相互独立的模块,以便用于每个模块只包含与其功能相关的内容,如登录模块、首页模块等
组件:指单一的功能组件,如登陆组件、视频组件等,每个组件都可以以单独的module开发,且可以单独抽出来作为SDK对外发布使用,一个模块中包含一个或多个组件
组件化的优势:
加快编译速度:每个业务功能都是一个单独的功能,可独立编译运行,编译速度快
解耦:将App分离成多个模块,每个模块都是一个组件
提高开发效率:每个组件模块由单人负责,降低开发沟通成本,减少因代码不一产生的影响
代码复用:类似引用第三方库,可以将组件剥离,在新项目微调或直接使用
组件化要解决的问题:组件分层:如何将项目分成多个组件,组件间的依赖关系是什么样的
组件单独运行和集成调试:组件如何独立运行和集成调试
组件间通信:主项目与组件、组件和组件之间如何通信
组件分层
组件依赖关系是上层依赖下层,修改频率是上层高于下层
主工程(app)→业务组件层→功能组件层
↓
lib_common
↓
基础组件层
- 主工程(app):只依赖各组件,通过配置指定依赖哪些组件;除了一些全局配置和主Activity外,不包含任何业务代码,是应用的入口
- 业务组件层:各组件之间无直接关联,通过路由进行通信;可直接依赖基础组件层,同时也能依赖公用的一些功能组件;业务组件可以在library和application之间切换,但最后打包时必须是library
- 功能组件层:依赖基础组件层;对一些公用的业务功能进行封装与实现
- common组件(lib_common):支撑业务组件、功能组件的基础;依赖基础组件层;业务组件、功能组件所需的基础能力只需依赖commmon组件即可获得
- 基础组件层:封装公用的基础组件(并实现了路由);网络访问框架、图片加载框架等主流的第三方库;各种第三方SDK
组件化项目搭建
application插件:如果一个模块被声明为application,它就会成为一个apk文件,可以直接安装运行
library插件:被声明为library的模块,会成为aar文件,不可以单独运行
Gradle配置统一管理
创建一个config.gradle文件用于全局管理版本号、依赖项等,避免项目中不同模块间依赖版本不一致的问题。
java
//在 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",
"login": "com.example.module.login",
"net": "com.example.module.mylibBase"
]
//定义了一些常见的 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"
}
在project的build.gradle中引入config.gradle文件
java
plugins {
alias(libs.plugins.android.application) apply false
}
apply from:"config.gradle"
将isDebug=true加入到gradle.properties中
业务组件创建及配置build.gradle文件
创建module时有两个选项
- Phone&Tablet:创建可以单独运行的模块,即业务模块,是一个单独的application,可以在library和application之间切换
- Android Library:创建一个Library,一般用于创建基础层,不需要单独运行
可以创建一个包,将组件全部放入里面管理,在创建时在Package name中再加一层module,防止在导包时出现冲突
修改buildle.gradle文件
java
/*plugins {
//将静态启动模式修改为动态启动模式
id 'com.android.application'
}*/
//动态配置插件,注意gradle.properties中的数据类型都是String类型,使用其他数据类型需要自行转换
if(isDebug.toBoolean()){
apply plugin: 'com.android.application'
}else{
apply plugin: 'com.android.library'
}
//简化手写,rootProject.ext的次数,提高性能
def cfg=rootProject.ext
android {
//命名空间
namespace cfg.applicationId.login
compileSdkVersion cfg.android.compileSdkVersion
defaultConfig {
//在defaultConfig中判断是否需要应用标识符,如果是调试模式则需要一个标识符启动这个单独的模块
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_11
targetCompatibility JavaVersion.VERSION_11
}
//选择合适的manifest,指定Android项目的源极配置,用于指定在构建Android应用时要使用的源代码、资源和清单文件的位置
sourceSets {
main {
//在debug模式下需要详细的AndroidManifest去启动一个活动
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')
implementation libs.appcompat
implementation libs.material
implementation libs.activity
implementation libs.constraintlayout
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
因为在debug模式下需要详细的AndroidManifest去启动一个活动,所以在该model的main包下新建一个debug包,并将自动生产的AndroidManifest拷贝到debug包下
在打包模式下,就不需要其他的活动清单里,索引将自动生成的AndroidManifest修改为以下模式
xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name=".MainActivity"
android:exported="true">
</activity>
</application>
</manifest>
sourceSets 定义了一个 main 源集。
对于 main 源集,使用了一个条件语句(if (cfg.isDebug))来决定使用哪个清单文件。
"/debug/ ": 这是一个 Ant 风格的通配符。** 表示匹配任意数量的目录,因此 /debug/ 表示匹配任何包含 "debug" 目录的路径。
基础组件创建及配置
java
plugins {
alias(libs.plugins.android.library)
}
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"
consumerProguardFiles "consumer-rules.pro"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
}
dependencies {
api cfg.library.appcompat
api cfg.library.material
api cfg.library.constraintlayout
api cfg.libGson
//也可以使用这样的方式一行代码完成:,相当于一个for循环遍历library
//library.each{k,v->implementation v}
implementation libs.appcompat
implementation libs.material
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
}
在 Gradle 构建中,rootProject.ext是指向项目根目录的引用,在dependencies引入在最外部定义的公用依赖。
这里采用Api的方式引用,这样做的好处是依赖可以传递。
比如A模块使用api导入了x依赖,B模块此时只需要导入A模块就可以使用x依赖。
app层配置
java
plugins {
alias(libs.plugins.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"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
buildFeatures {
viewBinding true
}
}
dependencies {
implementation project(':modulesBase:libBase')
if (!isDebug.toBoolean()){
implementation project(":modulesCore:live")
implementation project(":modulesCore:login")
}
implementation libs.appcompat
implementation libs.material
implementation libs.constraintlayout
implementation libs.navigation.fragment
implementation libs.navigation.ui
testImplementation libs.junit
androidTestImplementation libs.ext.junit
androidTestImplementation libs.espresso.core
}
主要是在dependencies中导入基础模块的依赖,并且还需要导入其他组件的依赖
在这里需要判断是否为debug模式,是的化就不用导入这些组件
组件间跳转
在组件化开发中,不允许组件层模块横向依赖,所以不能直接访问彼此的类,需要通过路由的方式跳转。
自定义路由
在base包下新建一个libRouter模块
新建一个Java文件实现路由功能
Router要是单例模式,才能保证每次共用的是同一个对象共享相同的数据。
java
public class Router {
private final static class Holder{
static Router INSTANCE=new Router();
}
public static Router getInstance(){
return Holder.INSTANCE;
}
}
Router的作用是管理不同的类,可以统一协调和使用这些类,因此我们用Map存储这些类和其包名路径的对应关系。
groupMap(key,value)
key:GroupName(类的包名)
value:routerMap(key , value)
↓ ↓
Path(路径名,用于寻找具体的类)Class<?>cla(用于存储指定的类)
java
static Map<String,Map<String,Class<?>>> groupMap=new HashMap<>();
Map<String,Class<?>> routerMap=new HashMap<>();
之后设计这些类的注册方法,需要一个Path和Class
java
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);
}
}
实现跳转
java
/**
* @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));
}
}
}
}
之后在App下的Activity实现注册,就能在其他模块调用startActivity实现跳转
java
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);
}
}
ARouter
ARouter是一个可以用于组件间跳转的第三方库,支持模块间的路由、通信和解耦
添加依赖
先在统一的config.gradle中添加版本等信息
java
ext{
...
libARouter= 'com.alibaba:arouter-api:1.5.2'
libARouterCompiler = 'com.alibaba:arouter-compiler:1.5.2'
}
之后配置libBase
java
dependencies {
...
api cfg.libARouter
...
}
再配置其他组件(如login)
java
android {
defaultConfig {
...
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
//如果项目内有多个annotationProcessor,则修改为以下设置
//arguments += [AROUTER_MODULE_NAME: project.getName()]
}
}
}
}
dependencies {
//arouter-compiler的注解依赖需要所有使用 ARouter 的 module 都添加依赖
annotationProcessor cfg.libARouterCompiler
...
}
在项目最外面的gradle.properties下加入这段代码:
java
android.useAndroidX=true
android.enableJetifier=true
添加注解
在要跳转的Activity之前加上注解
java
// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path="/login/LoginActivity")
public class LoginActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
Log.d("login","login");
}
}
初始化SDK
java
public class App extends Application {
public void onCreate(){
super.onCreate();
if(isDebug()){//这两行代码必须写在init之前,否则这些配置无效
ARouter.openLog();//打印日志
ARouter.openDebug();//开启调试模式
}
ARouter.init(this);//对ARouter进行初始化
}
private boolean isDebug(){
return BuildConfig.DEBUG;
}
public final class BuildConfig{
public static final boolean DEBUG=false;
public static final String LIBRARY_PACKAGE_ANME="com.alibaba.android.arouter";
public static final String BUILD_TYPE="release";
public BuildConfig(){
}
}
}
BuildConfig可以将gradle的数据映射到Java类里面,实现在java代码中使用gradle数据
并且在AndroidManifest.xml中注册这个application
发起路由操作
之后就可以在类中使用跳转
java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
ARouter.getInstance().build("/login/LoginActivity").navigation();
}
}
实现不同组件间的数据传递
ARouter在跳转时可以携带参数
java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
ARouter.getInstance().build("/login/LoginActivity")
.withString("key1","app")
.withBoolean("key2",false)
.withSerializable("key3",new User("mm",5))
.navigation();
}
}
其中User是在基础组件中定义的一个实现了Serializable接口的类
java
public class User implements Serializable {
private String name;
private int cnt;
public User(String name,int cnt){
this.cnt=cnt;
this.name=name;
}
public String getName() {
return name;
}
}
之后就可以在跳转到的类中接收参数,注意接收的参数必须使用@Autowired 注解
java
@Route(path="/login/LoginActivity")
public class LoginActivity extends AppCompatActivity {
@Autowired
public String key1;
@Autowired
public boolean key2;
@Autowired
public User key3;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
Log.d("login","login");
ARouter.getInstance().inject(this);
Log.e("TAG","key1"+key1);
Log.e("TAG","key2"+key2);
Log.e("TAG","key3"+key3.getName());
}
}