前言
之前考虑到后续可能会参与到小包项目的开发中,因此希望利用平时的时间形成2套框架,后续新项目立项后可立即投入使用,快速上线。本文说明的是一套基于jetpack+coroutines基础上形成的一套组件化框架,本文主要描述组件化项目的通用配置和关键技术点的解决方案。
为什么要组件化
大家知道,随着项目的业务逐渐发展,业务功能越来越多,参与的开发人员也可能变更,修改合并代码容易出现冲突,于是在出现了模块化的概念,即把同一个业务板块的内容单独抽取一个module去实现,做到代码隔离。但是模块化也会带来一些新的问题,不同的模块之间为了实现数据跳转,依赖关系错综复杂,代码耦合性很强。而且最致命的是,项目迭代到一定程度,由于项目本身比较大,全量编译耗时往往非常巨大,因此引入组件化。
总结下来组件化有如下2个优点:
- 加快编译速度。在开发阶段,业务模块(module_xxx)可以作为application独立编译,编译速度明显加快;
- 明确开发人员彼此职责。明确开发人员维护模块,只需要管理和熟悉相应的module,彼此互不打扰,公共基础功能根据是否是业务相关,降级到Common层或core层,或单独封装成lib调用;
- 更合规。相比插件化的解决方案,组件化并没有引入动态加载Apk的功能,保证在google play等商店上线时不会出现合规性问题;
项目架构图
以上架构图是目前我完成的项目中的模块依赖关系图,各个模块承载功能说明如下:
App壳module中没有任何业务代码,只有全局Application对象的代码,和全局主题、logo等配置;
Module_main、Module_sample、Module_A、Module_B为4个业务模块,后续可根据项目中真实的模块做修改;
lib_download代表下载功能,是一个library,某个业务模块如需要直接依赖即可;
lib_common作为项目业务模块的业务公共组件,包括所有和项目界面相关的组件或基类;
lib_core代表所有与项目界面无关的组件和类库及资源,包括基类、图片加载、日志、网络请求、基础Util等
lib_fileprovider代表fileprovider的公共定义处理,common库直接依赖即可;
lib_webview代表webview页面的模块,由于业务模块基本都会跳转webview,所以直接common库去依赖;
组件化配置
-
在项目中的gradle.properties中添加组件化控制开关
#当前是否是组件化模块,false表示整体编译,true表示分模块编译 isComponentMode=false
在开发阶段,将此参数调整为true,即可单个模块编译打包,合并打包测试或发布时,将此参数修改为false
-
新增模块调试入口启动类
在module_XXX等业务模块下新建Debug包,并新增DebugActivity和XXXApplication作为组件化状态下,模块调试的入口类和Application对象,如下
-
新增模块编译时的Manifest文件
在module_XXX等业务模块下新建module_manifest文件夹,提供AndroidManifest文件,此Manifest文件包含上面的DebugActivity文件作为App启动入口,及其他当前模块的所有Activity
抽取module_main、module_A、module_B等业务模块的build.gradle文件中公共内容,形成一个模块通用的build.gradle文件,样例如下
if (Boolean.parseBoolean(isComponentMode)) {
apply plugin: 'com.android.application'
} else {
apply plugin: 'com.android.library'
}
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt'
android {
compileSdkVersion configs.android.compileSdkVersion
buildToolsVersion configs.android.buildToolsVersion
defaultConfig {
minSdkVersion configs.android.minSdkVersion
targetSdkVersion configs.android.targetSdkVersion
versionCode configs.android.versionCode
versionName configs.android.versionName
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}
}
sourceSets {
main {
jniLibs.srcDirs = ['libs']
if (Boolean.parseBoolean(isComponentMode)) {
//模块化,作为独立App应用运行
manifest.srcFile 'src/main/module_manifest/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/AndroidManifest.xml'
resources {
//合并打包版本时,排除debug文件夹下所有文件
exclude '*/debug/*'
}
}
}
}
buildTypes {
debug {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
buildFeatures {
viewBinding true
}
}
注意点:
此文件顶部的module类型配置,通过步骤一中的参数动态改变模块的module类型
sourceSets中通过步骤一的参数动态获取AndroidManifest文件
- 然后在对应的module_xxx中的build.gradle顶部添加此公共文件的依赖,样例如下:
apply from: "../module.build.gradle"
android {
defaultConfig {
if (Boolean.parseBoolean(isComponentMode)) {
//Module作为独立App应用运行,需配置包名
applicationId "com.zhl.module_main"
}
}
}
dependencies {
implementation project(path: ':lib_common')
//ARouter注解处理器
kapt deps.arouter.arouter_compiler
}
- 壳工程依赖。设置在非组件化模式下,依赖各个业务模块合并编译
dependencies {
implementation fileTree(dir: "libs", include: ["*.jar"])
if (!Boolean.parseBoolean(isComponentMode)) {
//非组件化模式下,依赖各个模块
implementation project(path: ':module_main')
implementation project(path: ':module_sample')
implementation project(path: ':module_a')
implementation project(path: ':module_b')
}
}
- 页面跳转
组件化的第一目标就是业务模块间解耦,不相互依赖,如模块A的的一个Activity需要跳转模块B的一个Activity,在没有相互依赖的情况下,我们采用阿里的路由解决方案-ARouter,ARouter的配置过程参照官方文档,需要注意的点是kapt相关的依赖配置需要在各个业务模块module去添加,代码参考如下:
dependencies {
implementation project(path: ':lib_common')
//ARouter注解处理器
kapt deps.arouter.arouter_compiler
}
defaultConfig {
minSdkVersion configs.android.minSdkVersion
targetSdkVersion configs.android.targetSdkVersion
versionCode configs.android.versionCode
versionName configs.android.versionName
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles "consumer-rules.pro"
kapt {
arguments {
arg("AROUTER_MODULE_NAME", project.getName())
}
}
}
然后在各个模块的Activity或Fragment上配置对应的path,则可实现跨模块跳转;
至此,可在修改isComponentMode参数不同的状态下实现分模块组件化编译运行或合并编译运行!
框架组件化技术点分析
通过上面的步骤相信你可以很快实现一个组件化框架的通用配置,但是如果你进一步深入组件化的使用后,会出现如下几个问题:
问题一:组件间数据传递
组件间的数据传递又分为两种情况:
1.页面主动跳转携带数据
组件化项目在业务开发过程中会出现如下问题:module_A和module_B之间要进行跳转,要使用一个共同的model,此时需要把该model对象放在Base中,但是随着业务的不断迭代,各个模块跳转之间跳转的业务会越来越多,导致model对象在base中越来越多,这部分内容严格来说是隶属于业务代码,因此在base模块和业务模块之间新增lib_common模块,统一管理Model实体类、图片资源、Strings等内容
2.模块数据被动拉取
例如:在module_main模块需要获取到module_user模块的用户信息去做显示。此场景使用ARouter的依赖注入进行处理
-
先在lib_common模块建立一个获取user信息的接口
interface IUserProvider : IProvider { /** * 提供User模块的数据 * * @param s */ fun getUserData(): String }
-
然后在module_user模块实现
@Route(path = ARouterConstant.USER.USER_DATA_PROVIDER) class UserProvider : IUserProvider { override fun getUserData(): String { return "这个是User模块的数据" } override fun init(context: Context?) { } }
-
再module_main模块获取数据
val iSampleProvider = ARouter.getInstance() .build(ARouterConstant.SAMPLE.SAMPLE_DATA_PROVIDER).navigation() as ISampleProvider Log.d("TAG值",iSampleProvider.getSampleData())
问题二: Application生命周期处理
Application作为程序启动的入口,其中的onCreate()方法经常做一些初始化相关工作,这些初始化的代码中,又包含2类,其中一类是所有模块都要用到的全局配置,如ARouter初始化、网络请求头配置、刷新样式、日志打印配置等。另外一类是某个模块特定的初始化配置。目前在组件化框架中有如下三种实现方式
方式一:统一处理
将全局配置代码统一全部放在Common层的Application的基类中,某个模块特定的初始化配置代码放在app壳module的Application配置
优点:处理简单,逻辑清晰
缺点:模块特定的初始化代码需要在其他module中调用,无法实现代码隔离
方式二: 反射处理
-
在lib_common模块中定义ICommonApplication
interface ICommonApplication { fun onCreate() }
-
定义业务module配置类
object ModuleApplicationConfig { private const val MODULE_MAIN = "com.zhl.module_main.MainApplication" private const val MODULE_SAMPLE = "com.zhl.module_sample.SampleApplication" private const val MODULE_A = "com.zhl.module_a.AApplication" private const val MODULE_B = "com.zhl.module_b.BApplication" val modules = mutableListOf
(MODULE_MAIN, MODULE_SAMPLE, MODULE_A, MODULE_B) }
-
在每个业务模块中定义Application回调接收
class MainApplication : ICommonApplication { override fun onCreate() { Log.d("Application生命周期", "MainApplication执行onCreate!!") //todo 执行Main_module模块需要特定执行的代码 } }
-
在CommonApplication类的OnCreate()方法中通过反射各个业务module的Application实例,然后执行相关初始化代码
open class CommonApplication : BaseApplication() { override fun onCreate() { super.onCreate() initComponent() } private fun initComponent() { ModuleApplicationConfig.modules.forEach { val clazz = Class.forName(it) val instance = clazz.newInstance() as ICommonApplication instance.onCreate() } } }
优点:可实现代码隔离,逻辑清晰
缺点:通过反射实现,有性能问题,新增模块是需要修改module配置类
方式三:APT处理
此方法引入一个新的AppLifeCycle插件,他可以无侵入的获取到Application的生命周期,具体使用步骤如下:
-
lib_common模块依赖applifecycle-api依赖
//AppLifecycle api 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-api:1.0.4'
-
各个业务模块module_xxx添加applifecycle-compiler注解处理器依赖(注意:java请用annotationProcessor关键字)
kapt 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-compiler:1.0.4'
-
新建各个业务模块的Application,用于接受主Application的生命周期事件分发,以module_main模块的MainApplication为例:
@AppLifecycle class MainApplication : IApplicationLifecycleCallbacks { /** * 设置优先级 * * @return */ override fun getPriority(): Int { return NORM_PRIORITY } override fun onCreate(context: Context?) { Log.d("Application生命周期", "MainApplication执行onCreate!!") } override fun onTerminate() { } override fun onLowMemory() { } override fun onTrimMemory(level: Int) { } }
-
壳工程添加AppLifecycle插件配置,并注册生命周期事件
项目build.gradle修改如下:
buildscript { ext.kotlin_version = "1.5.21" repositories { google() mavenCentral() //applifecycle插件仓也是jitpack maven { url 'https://jitpack.io' } } dependencies { classpath 'com.android.tools.build:gradle:7.0.3' classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.31' //加载插件applifecycle classpath 'com.github.hufeiyang.Android-AppLifecycleMgr:applifecycle-plugin:1.0.3' } }
app模块build.gradle修改如下:
//使用插件applifecycle apply plugin: 'com.hm.plugin.lifecycle'
优点:实现代码隔离,通过APT插件模式处理,编译阶段完成引用,无性能问题
缺点:代码逻辑调用无直接关联,要引入三方库
结论:目前项目经过评估引入方式三处理
其他
具体代码见:https://github.com/henryzhu-dev/AppArchitectureApplication
目前还在持续不断完善中,力求可以覆盖更多场景,实现最终快速实现业务,高质量交付的目的!
参考资料:
https://juejin.cn/post/6881116198889586701
https://juejin.cn/post/6844904147641171981
https://juejin.cn/post/6844904147641171981#heading-6
https://zhuanlan.zhihu.com/p/333887361