发布时间:2023-04-03 10:30
所谓的组件化就是把需求拆成一个一个的小模块,最后组装需要的模块组成一个app
创建一个 Project 后可以创建多个 Module,这个 Module 就是所谓的模块。一个的例子,在写代码的时候我们会把每个模块拆开,每个 tab 所包含的内容就是一个模块,这样可以减少 module 的代码量,但是每个模块之间的肯定是有页面的跳转,数据传递等。
加快编译速度:每个业务组件都可以单独运行调试,速度提升好几倍
功能重用:一次编码处处复用,再也不需要复制代码了。基础组件和业务基础组件,调用者可以根据文档就可以一键集成和使用。
提高协作效率:每个组件都有专人维护,只需要重点测试修改的组件即可。
1、全局配置
全局配置 :
isModuleMode 是最终要的配置 , 通过该配置的 true / false 设置当前是否开启组件化
androidConfig 用于统一管理各个 Module 中的版本号 , 如编译版本号 , 最小版本号 , 目标版本号 ;applicationId 用于保存各个模块的包名 , 尤其是 module 依赖库的包名 , 组件化的状态下 , 该 module 需要独立运行 , 必须配置一个 applicationId 包名 。
dependencies 用于统一管理各个模块之间的依赖库 , 避免管理分散 ;
// ext 是 extension 扩展的含义
// ext 后的 {} 花括号 , 是闭包 ,
ext{
// 是否是模块化模式
// 集成模式 true ( 默认模式 , 模块化 )
// 组件模式 false ( 组件化 )
isModuleMode = true
// 定义 android 变量 , 类型是字典 Map 集合
// 其中定义了若干键值对集合
androidConfig = [
compileSdkVersion : 30,
minSdkVersion : 18,
targetSdkVersion : 30,
versionCode : 1,
versionName : \"1.0\"
]
applicationId = [
\"app\" : \"kim.hsl.component\",
\"module1\" : \"kim.hsl.module1\",
\"module2\" : \"kim.hsl.module2\",
]
// androidx 版本号
androidxVersion = \"1.3.0\"
constraintlayoutVersion = \"2.0.4\"
materialVersion = \"1.3.0\"
// 统一管理依赖库
dependencies = [
// ${} 表示引用之前定义的变量
\"appcompat\" : \"androidx.appcompat:appcompat:${androidxVersion}\",
\"constraintlayout\" : \"androidx.constraintlayout:constraintlayout:${constraintlayoutVersion}\",
\"material\" : \"com.google.android.material:material:${materialVersion}\"
]
}
2、工程下的 build.gradle 配置
在总的 build.gradle 配置中 , 引入上述全局配置 , 其作用就相当于将上述全局配置原封不动拷贝过来 ;
apply from: \"component.gradle\"
完整配置 :
// Top-level build file where you can add configuration options common to all sub-projects/modules.
// 将 component.gradle 配置文件中的内容导入到该位置
// 相当于引入头文件
apply from: \"component.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
}
}
allprojects {
repositories {
google()
jcenter()
}
}
task clean(type: Delete) {
delete rootProject.buildDir
}
3、app 模块下的 build.gradle 配置
在 app 模块中重点关注 , 在组件模式下 , 一定不能引入依赖库 , 否则会报错 , 因为组件模式下这两个依赖库是两个可运行的独立应用 ;
dependencies {
if (isModuleMode){
// 集成模式下才能引用这两个 Library Module
implementation project(path: \':module1\')
implementation project(path: \':module2\')
}
}
版本号 , applicationId , 依赖库 统一管理 :
从 Project 级别的配置中获取变量 :
def androidConfig = rootProject.ext.androidConfig
def appId = rootProject.ext.applicationId
def dep = rootProject.ext.dependencies
版本号 和 applicationId 统一管理 :
android {
compileSdkVersion androidConfig.compileSdkVersion
defaultConfig {
applicationId appId[\"app\"]
minSdkVersion androidConfig.minSdkVersion
targetSdkVersion androidConfig.targetSdkVersion
versionCode androidConfig.versionCode
versionName androidConfig.versionName
}
依赖库统一管理 :
dependencies {
//implementation \'androidx.appcompat:appcompat:1.3.0\'
//implementation \'androidx.constraintlayout:constraintlayout:2.0.4\'
//implementation \'com.google.android.material:material:1.3.0\'
implementation dep.appcompat
implementation dep.constraintlayout
implementation dep.material
}
完整配置 :
plugins {
id \'com.android.application\'
}
def androidConfig = rootProject.ext.androidConfig
def appId = rootProject.ext.applicationId
def dep = rootProject.ext.dependencies
android {
compileSdkVersion androidConfig.compileSdkVersion
buildToolsVersion \"30.0.3\"
defaultConfig {
applicationId appId[\"app\"]
minSdkVersion androidConfig.minSdkVersion
targetSdkVersion androidConfig.targetSdkVersion
versionCode androidConfig.versionCode
versionName androidConfig.versionName
testInstrumentationRunner \"androidx.test.runner.AndroidJUnitRunner\"
// ARoute 需要的配置
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
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 \'androidx.appcompat:appcompat:1.3.0\'
//implementation \'androidx.constraintlayout:constraintlayout:2.0.4\'
//implementation \'com.google.android.material:material:1.3.0\'
implementation dep.appcompat
implementation dep.constraintlayout
implementation dep.material
testImplementation \'junit:junit:4.+\'
androidTestImplementation \'androidx.test.ext:junit:1.1.2\'
androidTestImplementation \'androidx.test.espresso:espresso-core:3.3.0\'
// 替换成最新版本, 需要注意的是api
// 要与compiler匹配使用,均使用最新版可以保证兼容
api \'com.alibaba:arouter-api:1.5.1\'
annotationProcessor \'com.alibaba:arouter-compiler:1.5.1\'
if (isModuleMode){
// 集成模式下才能引用这两个 Library Module
implementation project(path: \':module1\')
implementation project(path: \':module2\')
}
}
所谓的单工程方案就是把所有组件都放到一个工程下
单工程利弊分析:
首先在 gradle.properties 文件内声明一个变量:
// gradle.properties
isModule = true
isModule 为 true 时表示组件可以作为 apk 运行起来,false 表示组件只能作为 library。我们根据需要改变这个值后同步下gradle即可。
然后在某个 module 的 build.gradle 文件内用这个变量做三个地方的判断:
// build.gradle
// 区分是应用还是库
if(isModule.toBoolean()) {
apply plugin: \'com.android.application\'
}else {
apply plugin: \'com.android.library\'
}
android {
defaultConfig {
// 如果是应用需要指定application
if(isModule.toBoolean()) {
applicationId \"com.xxx.xxx\"
}
}
sourceSets {
main {
// 应用和库的AndroidManifest文件区分
if(isModule.toBoolean()) {
manifest.srcFile \'src/main/debug/AndroidManifest.xml\'
}else {
manifest.srcFile \'src/main/AndroidManifest.xml\'
}
}
}
}
由于library是不需要 Application 和启动Activity页,所以我们要区分这个文件,应用manifest指定的路径没有特定,随意找个路径创建即可。在应用AndroidManifest.xml里我们要设置启动页:
library 的 AndroidManifest.xml 不需要这些:
gradle 依赖 module 的方式主要有两种:
一般来说我们只需要使用 implementation 即可,api 是会造成项目编译时间变长,而且会引入该模块不需要的功能,代码之间耦合变得严重了。不过 module_common 是统一了基础组件版本的公共库,所有组件都应需要依赖它并拥有基础组件的能力,所以基本每个业务组件和业务基础组件都应该依赖公共库:
dependencies {
implementation project(\':module_common\')
}
而 common 组件依赖基础组件应该是用 api,因为把基础组件的能力传递给上层业务组件:
dependencies {
api project(\':module_base\')
api project(\':module_util\')
}
多工程就是每个组件都是一个工程,例如创建一个工程后 app 作为壳组件,它依赖 biz_home 运行,因此不需要 isModule 来控制独立调试,它本身就是一个工程可以独立调试。
多工程的利弊就是和单工程相反的:
多工程组件依赖需要用到maven仓库。把每个组件的aar上传到公司内网的maven仓库,然后像这样去依赖:
implementation \'com.xxx.xxx:module_common:1.0.0\'
我们把三方库统一放到 config.gradle 内管理:
ext {
dependencies = [
\"glide\": \"com.github.bumptech.glide:glide:4.12.0\",
\"glide-compiler\": \"com.github.bumptech.glide:compiler:4.12.0\",
\"okhttp3\": \"com.squareup.okhttp3:okhttp:4.9.0\",
\"retrofit\": \"com.squareup.retrofit2:retrofit:2.9.0\",
\"retrofit-converter-gson\" : \"com.squareup.retrofit2:converter-gson:2.9.0\",
\"retrofit-adapter-rxjava2\" : \"com.squareup.retrofit2:adapter-rxjava2:2.9.0\",
\"rxjava2\": \"io.reactivex.rxjava2:rxjava:2.2.21\",
\"arouter\": \"com.alibaba:arouter-api:1.5.1\",
\"arouter-compiler\": \"com.alibaba:arouter-compiler:1.5.1\",
// our lib
\"module_util\": \"com.sun.module:module_util:1.0.0\",
\"module_common\": \"com.sun.module:module_common:1.0.0\",
\"module_base\": \"com.sun.module:module_base:1.0.0\",
\"fun_splash\": \"com.sun.fun:fun_splash:1.0.0\",
\"fun_share\": \"com.sun.fun:fun_share:1.0.0\",
\"export_biz_home\": \"com.sun.export:export_biz_home:1.0.0\",
\"export_biz_me\": \"com.sun.export:export_biz_me:1.0.0\",
\"export_biz_msg\": \"com.sun.export:export_biz_msg:1.0.0\",
\"biz_home\": \"com.sun.biz:biz_home:1.0.0\",
\"biz_me\": \"com.sun.biz:biz_me:1.0.0\",
\"biz_msg\": \"com.sun.biz:biz_msg:1.0.0\"
]
}
这样方便版本统一管理, 然后在根目录的 build.gradle 内导入:
apply from: \'config.gradle\'
最后在各自的模块引入依赖,比如在 module_common 中这么引入依赖即可。
dependencies {
api rootProject.ext.dependencies[\"arouter\"]
kapt rootProject.ext.dependencies[\"arouter-compiler\"]
api rootProject.ext.dependencies[\"glide\"]
api rootProject.ext.dependencies[\"okhttp3\"]
api rootProject.ext.dependencies[\"retrofit\"]
api rootProject.ext.dependencies[\"retrofit-converter-gson\"]
api rootProject.ext.dependencies[\"retrofit-adapter-rxjava2\"]
api rootProject.ext.dependencies[\"rxjava2\"]
api rootProject.ext.dependencies[\"module_util\"]
api rootProject.ext.dependencies[\"module_base\"]
}
做完组件之间的隔离后,暴露出来最明显的问题就是页面跳转和数据通信的问题。一般来说,页面跳转都是显示startActivity跳转,在组件化项目内就不适用了,隐式跳转可以用,但每个Activity都要写 intent-filter 就显得有点麻烦,所以最好的方式还是用路由框架。
实际上市面已经有比较成熟的路由框架专门就是为了组件化而生的,比如美团的WMRouter,阿里的ARouter等,本例使用 ARouter 框架,看下ARouter页面跳转的基本操作。
首先肯定是引入依赖,以 module_common 引入ARouter举例,build.gradle 应该添加:
android {
defaultConfig {
javaCompileOptions {
annotationProcessorOptions {
arguments = [AROUTER_MODULE_NAME: project.getName()]
}
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
api rootProject.ext.dependencies[\"arouter\"]
kapt rootProject.ext.dependencies[\"arouter-compiler\"]
}
kapt注解依赖没有办法传递,所以我们不可避免得需要在每个模块都声明这些配置,除了 api
rootProject.ext.dependencies[“arouter”] 这行。然后需要全局注册 ARouter,我是在 module_common 统一注册的。
class AppCommon: BaseApp{
override fun onCreate(application: Application) {
MLog.d(TAG, \"BaseApp AppCommon init\")
initARouter(application)
}
private fun initARouter(application: Application) {
if(BuildConfig.DEBUG) {
ARouter.openLog()
ARouter.openDebug()
}
ARouter.init(application)
}
}
接着我们在 module_common 模块内声明一个路由表用作统一管理路径。
// RouterPath.kt
class RouterPath {
companion object {
const val APP_MAIN = \"/app/MainActivity\"
const val HOME_FRAGMENT = \"/home/HomeFragment\"
const val MSG_FRAGMENT = \"/msg/MsgFragment\"
const val ME_FRAGMENT = \"/me/MeFragment\"
const val MSG_PROVIDER = \"/msg/MsgProviderImpl\"
}
}
然后在MainActivity类文件上进行注解:
@Route(path = RouterPath.APP_MAIN)
class MainActivity : AppCompatActivity() {
}
任意模块只需要调用 ARouter.getInstance().build(RouterPath.APP_MAIN).navigation() 即可实现跳转。如果我们要加上数据传递也很方便:
ARouter.getInstance().build(RouterPath.APP_MAIN)
.withString(\"key\", \"value\")
.withObject(\"key1\", obj)
.navigation()
然后在MainActivity使用依赖注入接受数据:
class MainActivity : AppCompatActivity() {
@Autowired
String key = \"\"
}
在 export_biz_msg 组件下声明 IMsgProvider,此接口必须实现 IProvider 接口:
interface IMsgProvider: IProvider {
fun onCountFromHome(count: Int = 1)
}
然后在 biz_msg 组件里实现这个接口:
@Route(path = RouterPath.MSG_PROVIDER)
class MsgProviderImpl: IMsgProvider {
override fun onCountFromHome(count: Int) {
// 这里只是对数据进行分发,有监听计数的对象会收到
MsgCount.instance.addCount(count)
}
override fun init(context: Context?) {
// 对象被初始化时调用
}
}
在 biz_home 首页组件中发送计数:
val provider = ARouter.getInstance().build(RouterPath.MSG_PROVIDER).navigation() as IMsgProvider
provider.onCountFromHome(count)
可以看到其实和页面跳转的方式基本雷同,包括获取 Fragment 实例的方式也是这种。ARouter把所有通信的方式都用一种api实现,让使用者上手非常容易。
Application生命周期分发
当 app 壳工程启动Application初始化时要通知到其他组件初始化一些功能。这里提供一个简单的方式。
首先我们在 module_common 公共库内声明一个接口 BaseApp:
interface BaseApp {
fun onCreate(application: Application)
}
然后每个组件都要创建一个 App 类实现此接口,比如biz_home组件:
class HomeApp: BaseApp {
override fun onCreate(application: Application) {
// 初始化都放在这里
MLog.d(TAG, \"BaseApp HomeApp init\")
}
}
剩下最后一步就是从 app 壳工程分发 application 的生命周期了,这里用到反射技术:
val moduleInitArr = arrayOf(
\"com.sun.module_common.AppCommon\",
\"com.sun.biz_home.HomeApp\",
\"com.sun.biz_msg.MsgApp\",
\"com.sun.biz_me.MeApp\"
)
class App: Application() {
override fun onCreate() {
super.onCreate()
initModuleApp(this)
}
private fun initModuleApp(application: Application) {
try {
for(appName in moduleInitArr) {
val clazz = Class.forName(appName)
val module = clazz.getConstructor().newInstance() as BaseApp
module.onCreate(application)
}
}catch (e: Exception) {
e.printStackTrace()
}
}
}