安卓本地&云端组件化架构
背景
公司项目目前安卓原生端有三个项目,分别是APP、智能电视看板、智能手表终端
三个应用在业务端有不同的表现,但在底层架构(基础组件)上应用一套逻辑
所以目前的架构需求是:
抽离基础组件上传到maven,以依赖的方式集成到各个项目
在APP中按具体业务模块拆分业务组件,
做到各个模块的完全独立(互不依赖),且各个模块可以单独运行,也可组合运行(可插拔)
架构
以 APP为例,
将上面的两个大需求拆分后的组件架构:
其中本地模块为业务模块,互相独立,可作为模块被APP壳子所依赖,也可作为单独APP运行
云端模块为基础组件,不用作为APP单独运行且不关心业务,可在所有项目中被依赖作为基础组件
技术栈
技术栈 | 实现 |
---|---|
语言 | Kotlin |
设计模式 | MVVM |
依赖注入 | Koin |
模块初始化 | AndroidStartUp |
网络请求 | Retrofit+RetrofitUrlManager+Okhttp |
异步 | 协程 |
UI | databinding & viewbinding |
事件总线 | LiveEventBus |
路由 | DRouter |
存储 | MMKV & Room |
长链接 | WebSocket & RabbitMQ |
IPC | AIDL |
业务组件
业务组件需要能单独调试,也能集成打包,且这个过程中不能修改Kotlin代码
定义一个组件,需要
- build.gradle
- AndroidManifest.xml
- Application
- Activity
build.gradle
概念
project.plugins.apply('com.android.application')
可定义一个可运行的应用
project.plugins.apply('com.android.library')
可定义一个组件
当业务组件想单独作为一个应用运行时,需要动态将build文件修改
核心
gradle.properties是Gradle的静态配置文件,可在Sync Project时提供配置
在此文件中定义SYLINK_IS_BUILD_MODULE参数,
当参数为true时,代表单独调试业务组件,为false时,代表业务组件依赖于壳APP,集成调试
实现
在业务组件的build.gradle文件中,动态判断,实现根据SYLINK_IS_BUILD_MODULE动态切换apply插件和设置包名
1 | if (project.name == 'app' |
AndroidManifest.xml
概念
当作为应用运行时,需要指定Application和启动的Activity,并注册组件内Activity
当作为组件被依赖时,仅需要注册组件内Activity
核心
组件内有MainActivity作为启动的Activity,
宿主APP有LoginActivity作为启动的Activity,
由于定义了多个启动Activity,
当组件被依赖运行时,会出现AndroidManifest冲突
实现
使用xml的tools:node方法
在宿主APP的AndroidManifest中加入组件的启动Activity,并加上tools:node="remove"
属性,
这样当组件被依赖运行时,AndroidManifest合并时会剔除掉组件Activity的定义
1 | <activity |
Application
概念
每个应用都要有一个Application,可在其内写一些SDK的初始化方法
各个组件如何初始化决定了组件化是否可插拔
有些架构定义了一个Application在宿主app中,让宿主app依赖所有子模块,来实现子模块的初始化方法
这样就会导致子模块无法实现可插拔,即宿主app必须依赖所有子模块才能正常运行,这明显不满足组件化的思想
核心
在frame组件中定义SylinkBaseApp,在业务组件的AndroidManifest中都使用SylinkBaseApp
使用App Startup或代理SylinkBaseApp生命周期的方法实现组件内的初始化方法
实现
方案有两种,一种可以在frame中定义BaseApplication,并定义Application生命周期的回调接口,各个模块实现接口并注册到BaseApplication中,
通过接口来代理Application生命周期实现模块初始化。
这边使用第二种方法,即使用App Startup 可在应用启动时简单、高效地初始化组件
这个组件的原理是使用ContentProvider来实现初始化,ContentProvider作为Android的四大组件之一一直很鲜为人知,平时用到的也不多,
但ContentProvider可以用来做初始化工作,他的生命周期在Application的attachBaseContext后,onCreate之前。
App Startup创建了一个ContentProvider,并提供了初始化接口create,我们只需要实现这个接口并写上组件的初始化方法,就可实现组件在应用
启动时调用这些初始化方法,从而达到代码隔离的目标。
而android-startup 是提供一种在应用启动时能够更加简单、高效的方式来初始化组件,这个库是对App Startup的优化实现,支持了线程控制和多进程调用
如下是定义在event包内的android-startup的基类
由于主进程已经有默认定义的ContentProvider,但子进程没有,所以需要自定义一个MultipleProcessStartupProvider作为子进程的ContentProvider
由于组件之间有初始化顺序要求,所以定义一个BaseInit,后续其他模块的Init类必须在这个类初始化完之后进行,FrameInitDelegate为基础初始化代理类,
初始化一些所有组件都需要使用的工具类,如MMKV,Timber等
BaseInit如下:
1 | class BaseInit : AndroidStartup<Unit>() { |
BaseRemoteInit如下:
由于是在子进程运行,我们需要加入@MultipleProcess(":remote")
注解来定义开启进程
1 |
|
最后不要忘了在xml中定义配置类和子进程的ContentProvider
1 | <provider |
在其他模块中使用时,必须实现dependencies
接口,定义NetworkInit的onCreate方法在BaseInit初始化完成后进行
1 | class NetworkInit : AndroidStartup<Unit>() { |
通过android-startup我们就实现了组件间的初始化代码隔离,真正的做到了可插拔组件
Activity
概念
页面跳转不能使用原始的Intent形式,因为Intent形式会显示声明Activity::class.java
核心
使用路由的方式进行页面的跳转
实现
使用了DRouter 来实现页面间的路由跳转
在项目的build.gradle中加入
1 | dependencies { |
在组件的build.gradle中加入
1 | api "io.github.didi:drouter-api:2.1.3" |
在Activity上加入路由
1 |
|
跳转方法
1 | DRouter.build(HomeRoutePath.home_activity_path).start() |
Koin
概念
在组件化过程中,依赖注入一直是个难点,传统的Dagger2,AndroidDagger对组件化都不是很友好(需要改源码),亲测Hilt后发现实现起来也不方便。
最后使用了Koin,发现Koin作为依赖注入框架配置简单,又有Kotlin的原生支持,还针对Android有具体的API,非常友好
核心
每个依赖注入框架的概念都大同小异,有一个注入的入口,有一个仓库用来定义需要被注入的类,以及最后的注入方法
如果熟悉Dagger的同学,那么上述在Dagger中分别代表Component,Module,inject方法。
在Koin中,这分别代表startKoin,loadKoinModule 和 by inject()
其中startKoin属于整个Koin的初始化方法,会注入application,所以只能调用一次
loadKoinModule属于动态注入Module仓库的方法,可多次调用
注入方法由于DSL的存在和Koin对Android的支持优化,最后的注入方法已经被Koin封装了,所以我们直接使用by inject()注入使用即可
实现
依赖注入需要跟着项目/组件一起初始化,所以我们选择在android-startup中初始化Koin
在
BaseInit
中调用startKoin方法,KoinLogger是实现的Koin日志打印自定义类,androidContext是给Koin注入上下文,fragmentFactory是谷歌新推出的fragment有参构造方法的Fragment生产工厂,如果有使用到可具体了解,这边不再赘述。1
2
3
4
5startKoin {
logger(KoinLogger())
androidContext(context)
fragmentFactory()
}在各个模块中定义di包,其中放module类,新建xxxModule类存放普通的实例(如各个模块的ApiService),
新建xxxViewModelModule.kt类,存放viewModel类,
get<>()方法能从其他已经被注入的Module中获取实例,
用single定义的实例为单例
用viewModel定义的类可在后面注入时自动绑定activity / fragment的生命周期
用factory定义的类每次注入时都会创建一个新的单例
1
2
3
4
5
6
7val storageModule = module {
single<StorageApiService> {
get<Retrofit>().create(StorageApiService::class.java)
}
}下面的viewModel中的一个构造参数是StorageApiService,就可以通过get()方法获得上面定义的单例实例
1
2
3
4
5
6
7
8val storageViewModelModule = module {
viewModel {
StorageOutInViewModel(get(),get())
}
viewModel {
StoragePickViewModel(get(),get())
}
}在各个模块的Init类的onCreate方法中,调用loadKoinModules方法,参数为本模块的module数组
1
2
3
4
5
6
7class StorageInit : BaseAndroidStartup() {
override fun create(context: Context) {
loadKoinModules(listOf(storageViewModelModule, storageFragmentModule, storageModule))
}
}在activity中注入viewModel,by viewModel()是Koin定义的DSL,这样注入后viewModel即可自动绑定Activity生命周期
1
val viewModel: MaterialInventoryViewModel by viewModel()
这只是Koin最基础的用法,更多的可查看官网,相比起Dagger甚至是Hilt,Koin可以说是最简单,对组件化支持最友好的依赖注入框架了
基础组件
基础组件脱离业务,在公司所有项目中都适用,且需要上传到maven,以云端依赖的方式集成到项目中
只需要集成即可,不需要关心实现和初始化问题,符合迪米特原则(最少知道)
初始化的方法同业务组件,使用android-startup
上传至MAVEN
Gradle7.0之后,上传maven插件也进行了更新,maven plugin,uploadArchives已无法使用
使用maven-publish plugin插件进行上传
新建一个push.gradle,应用maven-publish插件进行上传,并定义组件库versionName
1
2
3
4
5//支持将项目发布到maven仓库的插件
apply plugin: 'maven-publish'
//def versionName = "1.0.0"
def versionName = "1.0.0-SNAPSHOT" //快照版本定义maven库的快照和release的URL
1
2def RELEASE_REPOSITORY_URL = "http://xxx.xxx.xxx.xxx:xxxx/repository/maven-releases/"
def SNAPSHOT_REPOSITORY_URL = "http://xxx.xxx.xxx.xxx:xxxx/repository/maven-snapshots/"编写上传方法,afterEvaluate为当构建后的操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29afterEvaluate {
publishing {
publications {
Production(MavenPublication) {
from components.release
groupId = "com.shuyilink"
artifactId = "dfs-app-frame"
version = versionName
// 上传source,这样使用放可以看到方法注释
artifact(sourcesJar)
artifact(assetsJar)
}
}
repositories {
// 定义一个 maven 仓库
maven {
// 可以有且仅有一个仓库不指定 name 属性,会隐式设置为 maven
// 根据 versionName 来判断仓库地址
url = versionName.endsWith('SNAPSHOT') ? SNAPSHOT_REPOSITORY_URL : RELEASE_REPOSITORY_URL
// 仓库用户名密码
credentials {
username = "xxx"
password = "xxx"
}
allowInsecureProtocol true
}
}
}
}编写上传源码的方法,这样在业务组件中可以点击查看基础组件的源码,而不是编译后的.class文件,方便调试
1
2
3
4
5
6
7
8
9
10
11task sourcesJar(type: Jar) {
if (project.hasProperty("kotlin")) {
from android.sourceSets.main.java.getSrcDirs()
} else if (project.hasProperty("android")) {
from android.sourceSets.main.java.sourceFiles
} else {
println project
from sourceSets.main.allSource
}
classifier = 'sources'
}当assets中有资源时,需编写上传assets的方法
1
2
3
4
5
6task assetsJar(type:Jar) {
classifier = "assets"
android.sourceSets.all { sourceSet ->
from sourceSet.assets.srcDirs
}
}当包中有aar或jar包依赖时,我们需要把这两个本地包一起打包,这边使用 fat-aar-android这个gradle插件可帮我们将aar和jar包打包进自己的aar包中
在项目的build.gradle中依赖fat-aar插件
1
2
3dependencies {
classpath 'com.github.kezong:fat-aar:1.3.6'
}在组件的build.gradle中应用插件,并将需要打包的aar或jar包使用embed替代implementation
1
2
3
4
5
6
7
8apply plugin: 'com.kezong.fat-aar'
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
embed(name: 'DataCollection', ext: 'aar')
embed(name: 'printer_release_v1.0_3', ext: 'aar')
}经过上述的配置后,在AndroidStudio右侧的Gradle配置中会有一个publishing的任务组,点击publish即可上传至maven,点击publishToMavenLocal即可上传到本地maven仓库
每个项目都有依赖仓库,App使用公司的maven仓库,所以需要在Repositories中加入公司maven仓库的地址
由于使用的是快照版本,gradle对SNAPSHOT版本存在缓存,会导致拉取快照依赖的时候并不是maven上最新的版本,
需要在每个业务模块的build.gradle中加入配置:
1
2
3
4//快照版本永远检查最新
project.configurations.all {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}经过上述操作后,最终我们依赖如下:
1
2
3dependencies {
implementation com.shuyilink:dfs-app-frame:1.0.0-SNAPSHOT
}
如此配置后,更改了基础组件只需要点击publish上传到maven,然后在业务组件中sync project拉取快照版本即可,无需关心快照的具体版本
build.gradle管理插件
问题
当模块增多时,每个模块都有一个单独的build.config配置,难以管理。
本地模块可在项目中编写一个version.gradle来统一管理依赖等信息,并在模块的build.gradle中
1 | apply from: '../version.gradle' |
这样每个本地模块都能共用version.gradle中的配置。
但上述方案只适用于本地模块,由于云端模块各自都有一个单独的项目,所以本地的version.gradle对云端模块就不适用了
所以编写了一个管理build的gradle插件并上传到maven仓库,使得所有模块可使用
实现
新建插件
在Android Studio中创建一个项目,并新建一个module,module类型选择 Java or Kotlin Library,并填写相关信息
新建完module后,在src/main下新建groovy文件夹,在groovy文件夹下新建包名文件夹,在src/main下新建resources文件夹,并在其下新建META-INF文件夹,再新建gradle-plugins文件夹,最终创建文件,包名.properties,包名必须与上面的groovy文件夹下的包名一致,如图:
在module的build.gradle文件中apply groovy插件和我们自己编写的上传至maven的push.gradle插件,并依赖gradleApi()和localGroovy()
1
2
3
4
5
6
7
8
9
10plugins {
id 'groovy'
}
apply from: '../push.gradle'
dependencies {
implementation gradleApi()
implementation localGroovy()
}在包名文件夹下新建XXXConrtoller.groovy文件,并实现
Plugin<Project>
,当项目apply了本插件并执行sync project时,就会自动执行apply中的逻辑1
2
3
4
5
6
7
8class DependenciesVersionController implements Plugin<Project> {
void apply(Project project) {
//TODO
}
}在包名.properties文件中,申明插件的Controller
1
com.sylink.dfs.app.version.plugin.DependenciesVersionController =
至此前期准备工作全部结束,接下来就是插件逻辑的编写
plugin统一定义
使用java和groovy编写gradle插件。例如在业务组件的build.gradle篇中所写到的动态切换 application 和 libray 在插件中实现时,新建AppPlugins.groovy文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19class AppPlugins {
static def addPlugins(Project project, SylinkProperties sylinkProperties) {
if (project.name == 'app'
|| (!sylinkProperties.getSYLINK_IS_JENKINS()
&& sylinkProperties.getSYLINK_IS_BUILD_MODULE()
&& project.name.contains('app'))
) {
project.plugins.apply('com.android.application')
project.plugins.apply('com.didi.drouter')
} else {
project.plugins.apply('com.android.library')
}
project.plugins.apply('kotlin-android')
project.plugins.apply('kotlin-kapt')
}
}在XXXController的apply方法中调用addPlugins方法
1
2
3
4
5
6
7
8
9class DependenciesVersionController implements Plugin<Project> {
void apply(Project project) {
SylinkProperties properties = PropertiesUtils.readProperties(project)
AppPlugins.addPlugins(project, properties)
}
}
build配置统一管理
定义AppInfoExt.java,写入android版本常量
1
2
3
4
5
6
7public class AppInfoExt {
public static int min_sdk = 24;
public static int target_sdk = 30;
public static int compile_sdk = 30;
public static String build_tools = "30.0.2";
}新建AppConfig.groovy,并定义addConfig方法,在addConfig方法中加入通用配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53class AppConfig {
static def addConfig(Project project, SylinkProperties sylinkProperties) {
def abiFiltersX86 = sylinkProperties.SYLINK_IS_DEBUG ? "x86" : ""
project.kapt {
useBuildCache = true
javacOptions {
option("-Xmaxerrs", 500)
}
}
//所有模块通用部分
project.android {
compileSdkVersion AppInfoExt.compile_sdk
buildToolsVersion AppInfoExt.build_tools
defaultConfig {
minSdkVersion AppInfoExt.min_sdk
targetSdkVersion AppInfoExt.target_sdk
versionCode sylinkProperties.SYLINK_VERSION_CODE
versionName sylinkProperties.SYLINK_VERSION_NAME
multiDexEnabled true
ndk {
//指定需要的os平台
abiFilters 'armeabi-v7a', abiFiltersX86
}
}
sourceSets {
main { jniLibs.srcDirs = ['libs'] }
}
lintOptions {
lintConfig project.file('lint.xml') //忽略检查
abortOnError false
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_11
targetCompatibility JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
}
}
}在addConfig中加入application模块的配置,在这个方法中可随意加入/拼接所需的配置,包括各种渠道配置,方便统一管理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72//如果是application模块
if (project.plugins.hasPlugin('com.android.application')) {
project.android {
signingConfigs {
release {
storeFile project.file(sylinkProperties.SYLINK_KEY_STORE)
storePassword sylinkProperties.SYLINK_KEY_STORE_PWD
keyAlias sylinkProperties.SYLINK_KEY_ALIAS
keyPassword sylinkProperties.SYLINK_KEY_ALIAS_PWD
}
}
buildTypes {
debug {
matchingFallbacks = ['release', 'debug']
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
minifyEnabled false
ext.enableCrashlytics = false
ext.alwaysUpdateBuildId = false
}
release {
matchingFallbacks = ['release', 'debug']
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
}
//开发用 MMdd 打包提测 MMddHH 因为精确到小时 跨小时就不能自动安装apk
def timeForDebugFileFormater = "MMdd"
def timeForReleaseFileFormater = "MMddHH"
flavorDimensions 'app'
applicationVariants.all { variant ->
variant.outputs.all {
def buildTypeName = variant.buildType.name
def timeFormater
if ("debug".equalsIgnoreCase(buildTypeName)) {
timeFormater = timeForDebugFileFormater
} else {
timeFormater = timeForReleaseFileFormater
}
outputFileName = "${AppInfoUtils.getApkName(sylinkProperties)}" +
"_${sylinkProperties.SYLINK_SERVER}_${buildTypeName}" +
"_V${sylinkProperties.SYLINK_VERSION_NAME}" +
"_${AppInfoUtils.releaseTime(timeFormater)}.apk"
}
}
}
}
//如果是业务的模块(有ApplicationID)
if (!sylinkProperties.SYLINK_APPLICATION_ID.isEmpty()
|| project.name == "lib.frame") {
project.android {
viewBinding {
enabled = true
}
dataBinding {
enabled = true
}
}
}最后在XXXController的apply方法中调用addConfig方法
1
2
3
4
5
6
7
8
9
10
11
12
13class DependenciesVersionController implements Plugin<Project> {
void apply(Project project) {
SylinkProperties properties = PropertiesUtils.readProperties(project)
println properties
AppPlugins.addPlugins(project, properties)
AppConfig.addConfig(project, properties)
}
}
IS_DEBUG
可以通过gradle的task任务来判断当前运行的是debug还是release模式
1 | static boolean isReleaseBuildType(Gradle gradle) { |
maven仓库统一配置
maven的配置也可在插件中统一设置
新建Repos.groovy,定义addRepos方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28class Repos {
static def addRepos(Project project, SylinkProperties sylinkProperties) {
Boolean USE_LOCAL_MAVEN = sylinkProperties.getSYLINK_USE_LOCAL_MAVEN()
Boolean isRelease = !sylinkProperties.getSYLINK_IS_DEBUG()
def handler = project.getRepositories()
//如果配置使用本地Maven资源且不是打Release包
if (USE_LOCAL_MAVEN != null
&& USE_LOCAL_MAVEN
&& !isRelease) {
println "使用本地Maven"
handler.mavenLocal()
}
handler.google()
handler.mavenCentral()
handler.maven { url 'https://jitpack.io' }
handler.maven {
url 'http://193.169.200.250:8081/repository/maven-public/'
allowInsecureProtocol = true
}
handler.maven { url 'https://oss.sonatype.org/content/repositories/snapshots' }
handler.maven { url "https://maven.aliyun.com/nexus/content/groups/public/" }
handler.maven { url "https://maven.aliyun.com/nexus/content/repositories/releases" }
}
}其中的使用本地Maven对应上传至Maven篇的publishToMavenLocal选项,将组件发布到本地maven,这在云端组件的调试阶段非常有用。
在XXXController的apply方法中调用addRepos方法,并且在上传至Maven篇中获得最新快照版本的配置也可统一写入这里
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class DependenciesVersionController implements Plugin<Project> {
void apply(Project project) {
SylinkProperties properties = PropertiesUtils.readProperties(project)
println properties
Repos.addRepos(project, properties)
//快照版本永远检查最新
project.configurations.all {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
AppPlugins.addPlugins(project, properties)
AppConfig.addConfig(project, properties)
}
}
依赖统一管理
新建Versions.java,用来定义依赖版本号
1
2
3
4
5
6
7public class Versions {
public static String android_gradle_plugin = "7.0.2";
public static String android_startup = "1.0.7";
public static String mmkv = "1.2.7";
//TODO 用到的依赖版本
}根据依赖分组建立依赖路径管理包,如Room下有四个依赖路径,新建Room.java文件
1
2
3
4
5
6public class Room {
public static String runtime = "androidx.room:room-runtime:"+ Versions.room;
public static String compiler_kapt = "androidx.room:room-compiler:"+Versions.room;
public static String testing = "androidx.room:room-testing:"+Versions.room;
public static String ktx = "androidx.room:room-ktx:"+Versions.room;
}根据不同组件建议依赖管理java文件,并定义依赖数组,如Event组件,新建EventExt.java文件,定义eventDepList数组,数组中存放的是 步骤2. 中的依赖路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17public class EventExt {
public static String[] eventDepList = {
Koin.android,
Koin.android_compat,
Support.android_startup,
Others.liveeventbus,
Others.timber,
Others.gson,
Kotlin.stdlib,
Kotlin.coroutines,
Others.mmkv,
Others.slf4j,
DRouter.api,
Retrofit.okhttp_logging_interceptor,
};
}在插件中新建ModuleEnum,放入各个组件的名字,方便判断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public enum ModuleEnum {
EVENT("lib.event"),
NETWORK("lib.network"),
DATABASE("lib.database"),
FRAME("lib.frame"),
SCAN("lib.scan"),
REMOTE("lib.remote"),
WEBSOCKET("lib.websocket"),
RABBITMQ("lib.rabbitmq");
private final String name;
ModuleEnum(String name) {
this.name = name;
}
public String getName() {
return name;
}
}新建AppDep.groovy,用于管理所有组件和项目的依赖项,以event组件为例,循环implementation定义的依赖,往往路径中有compiler的依赖是注解类依赖,需要使用katp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27class AppDep {
static def addDep(Project project, SylinkProperties sylinkProperties) {
if (sylinkProperties.SYLINK_APPLICATION_ID.isEmpty()) {
switch (project.name) {
case ModuleEnum.EVENT.name:
project.dependencies {
EventExt.eventDepList.each {
implementation it
}
}
break
case ModuleEnum.DATABASE.name:
project.dependencies {
DatabaseExt.databaseDepList.each {
if (it.contains("compiler")) {
kapt it
} else {
implementation it
}
}
}
break
}
}
}
}在XXXController的apply方法中调用addDep方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20class DependenciesVersionController implements Plugin<Project> {
void apply(Project project) {
SylinkProperties properties = PropertiesUtils.readProperties(project)
println properties
Repos.addRepos(project, properties)
//快照版本永远检查最新
project.configurations.all {
resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
}
AppPlugins.addPlugins(project, properties)
AppConfig.addConfig(project, properties)
AppDep.addDep(project, properties)
}
}
使用
在新建项目篇中已经介绍了插件集成了push.gradle,跟云端组件上传至Maven一样,在插件开发完成后点publish即可发布到Maven仓库中
需要注意的是这个gradle插件,不是android的项目,所以上传来源有所不同。
在android项目中,上传来源是components.release,而gradle插件则是components.java
1
2
3
4
5
6
7
8
9
10
11
12afterEvaluate {
publishing {
publications {
Production(MavenPublication) {
// from components.release 这是android组件的配置
from components.java
//TODO ...
}
}
}
}在项目的build.gradle文件中添加
1
2
3dependencies {
classpath 'com.shuyilink:dfs-app-version:1.0.0-XXXXXXX'
}在模块的build.gradle文件中添加
1
apply plugin: 'com.sylink.dfs.app.version.plugin'
这样所有的组件均可获得插件内的gradle配置
至此插件的基本开发结束,不同项目和不同组件可通过包名或组件名来做不同的动态配置
gradle.properties读取
问题
gradle.properties可以写入一些项目的配置常量,且能被build.gradle读取,但由于云端组件独立于每个项目之中,这就导致了每个组件有自己的一份
build.config文件,每个项目维护一份读取gradle.properties的方法很冗余,需要有统一读取项目的配置文件的方法。
实现
在所有模块都依赖上面的gradle插件后,可以将gradle.properties中的配置统一写进gradle插件中
在插件中定义PropertiesEnum,来定义gradle.properties中值的key
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15enum PropertiesEnum {
DESIGN_WIDTH,
DESIGN_HEIGHT,
SYLINK_COMPANY,
SYLINK_IS_BUILD_MODULE,
SYLINK_IS_JENKINS,
SYLINK_SERVER,
SYLINK_APPLICATION_ID,
SYLINK_USE_LOCAL_MAVEN,
SYLINK_VERSION,
SYLINK_KEY_STORE,
SYLINK_KEY_STORE_PWD,
SYLINK_KEY_ALIAS,
SYLINK_KEY_ALIAS_PWD,
}定义bean类SylinkProperties,将上面的值定义在bean类中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20public class SylinkProperties {
private String DESIGN_WIDTH;
private String DESIGN_HEIGHT;
private Boolean SYLINK_IS_DEBUG;
private String SYLINK_COMPANY;
private Boolean SYLINK_IS_BUILD_MODULE;
private Boolean SYLINK_IS_JENKINS;
private String SYLINK_SERVER;
private String SYLINK_APPLICATION_ID;
private String SYLINK_VERSION_NAME;
private Integer SYLINK_VERSION_CODE;
private Boolean SYLINK_USE_LOCAL_MAVEN;
private String SYLINK_KEY_STORE;
private String SYLINK_KEY_STORE_PWD;
private String SYLINK_KEY_ALIAS;
private String SYLINK_KEY_ALIAS_PWD;
//TODO 构造,get set方法
}编写方法读取项目中properties中的值并赋值到SylinkProperties中,properties中值的key必须跟
PropertiesEnum
中一样1
2
3
4
5
6
7
8
9
10
11
12class PropertiesUtils {
static SylinkProperties readProperties(Project project) {
HashMap proMap = project.properties
SylinkProperties sylinkProperties = new SylinkProperties()
sylinkProperties.setDESIGN_HEIGHT(proMap[PropertiesEnum.DESIGN_HEIGHT.name()])
sylinkProperties.setDESIGN_WIDTH(proMap[PropertiesEnum.DESIGN_WIDTH.name()])
sylinkProperties.setSYLINK_IS_DEBUG(!AppInfoUtils.isReleaseBuildType(project.gradle))
//TODO 其他项赋值
}
}在XXXController的apply方法中调用readProperties方法
1
2
3
4
5
6
7
8
9
10
11class DependenciesVersionController implements Plugin<Project> {
void apply(Project project) {
SylinkProperties properties = PropertiesUtils.readProperties(project)
println properties
}
}至此,只要在组件中依赖gradle插件,就能统一读取gradle.properties中的属性并通过properties实例使用
BuildConfig失效
问题
上传至maven的是aar包,aar包可以理解为加了资源文件的jar包,jar包中是编译后的java/kotlin文件,即.class文件。
这也是为什么只有云端依赖的组件化才能真正提高编译速度的原因
而BuildConfig是在编译中根据build.gradle的buildTypes中的配置项写入的。
既然aar包已经编译了,那云端组件的BuildConfig自然也就已经写入生成了,所以在云端组件中BuildConfig.DEBUG永远是false
需要寻找可替代BuildConfig的配置,使云端依赖也能跟随实际项目的配置而改变,比如服务器地址,UI自适应的宽高等
这个配置需满足两个条件:
- 能同时影响本地组件和编译后的云端组件(能被这两种组件共用)
- 在编译时根据build.gradle文件动态改变对应的值
实现
使用AndroidManifest.xml替代Build.Config,
对于上面两个条件
- 在编译时所有模块的AndroidManifest.xml会合并成一个文件,满足被所有组件共用的条件
- AndroidManifest.xml中的属性可以在build.gradle中通过manifestPlaceholders设置,且可通过反射被读出,满足动态改变的条件。
同样使用自定义的插件来实现所有模块的配置统一读写方法
动态配置属性均写在gradle.properties中,使用上面的gradle.properties读取篇,将配置写入sylinkProperties实例
根据build配置统一管理篇,在addConfig方法中加入配置:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//如果是application模块
if (project.plugins.hasPlugin('com.android.application')) {
project.android {
defaultConfig {
applicationId sylinkProperties.SYLINK_APPLICATION_ID
manifestPlaceholders = [
DESIGN_WIDTH : sylinkProperties.DESIGN_WIDTH,
DESIGN_HEIGHT : sylinkProperties.DESIGN_HEIGHT,
SYLINK_IS_DEBUG : sylinkProperties.SYLINK_IS_DEBUG,
SYLINK_IS_BUILD_MODULE: sylinkProperties.SYLINK_IS_BUILD_MODULE,
SYLINK_IS_JENKINS : sylinkProperties.SYLINK_IS_JENKINS,
SYLINK_SERVER : sylinkProperties.SYLINK_SERVER,
SYLINK_APPLICATION_ID : sylinkProperties.SYLINK_APPLICATION_ID,
SYLINK_VERSION_NAME : sylinkProperties.SYLINK_VERSION_NAME,
SYLINK_COMPANY : sylinkProperties.SYLINK_COMPANY,
]
}
}
}在项目壳App的AndroidManifest.xml中加入meta-data配置,vlaue必须与上面的key相对应
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25<meta-data
android:name="design_width_in_dp"
android:value="${DESIGN_WIDTH}" />
<meta-data
android:name="design_height_in_dp"
android:value="${DESIGN_HEIGHT}" />
<meta-data
android:name="SYLINK_IS_DEBUG"
android:value="${SYLINK_IS_DEBUG}" />
<meta-data
android:name="SYLINK_SERVER"
android:value="${SYLINK_SERVER}" />
<meta-data
android:name="SYLINK_IS_JENKINS"
android:value="${SYLINK_IS_JENKINS}" />
<meta-data
android:name="SYLINK_IS_BUILD_MODULE"
android:value="${SYLINK_IS_BUILD_MODULE}" />
<meta-data
android:name="SYLINK_COMPANY"
android:value="${SYLINK_COMPANY}" />
<meta-data
android:name="SYLINK_APPLICATION_ID"
android:value="${SYLINK_APPLICATION_ID}" />在event组件中添加getMetaData方法,由于这些配置所有组件都可能有用,所以将方法放在event组件内,方便全局调用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53object BuildConfigParseUtil {
const val KEY_SERVER = "SYLINK_SERVER"
const val KEY_IS_JENKINS = "SYLINK_IS_JENKINS"
const val KEY_IS_BUILD_MODULE = "SYLINK_IS_BUILD_MODULE"
const val KEY_COMPANY = "SYLINK_COMPANY"
const val KEY_IS_DEBUG = "SYLINK_IS_DEBUG"
const val KEY_APPLICATION_ID = "SYLINK_APPLICATION_ID"
const val KEY_VERSION_NAME = "SYLINK_VERSION_NAME"
fun getMetaData(context: Context) {
val packageManager = context.packageManager
val applicationInfo: ApplicationInfo
try {
applicationInfo = packageManager.getApplicationInfo(
context
.packageName, PackageManager.GET_META_DATA
)
if (applicationInfo != null && applicationInfo.metaData != null) {
if (applicationInfo.metaData.containsKey(KEY_SERVER)) {
SylinkConstants.SERVER =
applicationInfo.metaData[KEY_SERVER] as String
}
if (applicationInfo.metaData.containsKey(KEY_COMPANY)) {
SylinkConstants.COMPANY =
applicationInfo.metaData[KEY_COMPANY] as String
}
if (applicationInfo.metaData.containsKey(KEY_IS_JENKINS)) {
SylinkConstants.IS_JENKINS =
applicationInfo.metaData[KEY_IS_JENKINS] as Boolean
}
if (applicationInfo.metaData.containsKey(KEY_IS_BUILD_MODULE)) {
SylinkConstants.IS_BUILD_MODULE =
applicationInfo.metaData[KEY_IS_BUILD_MODULE] as Boolean
}
if (applicationInfo.metaData.containsKey(KEY_IS_DEBUG)) {
SylinkConstants.IS_DEBUG =
applicationInfo.metaData[KEY_IS_DEBUG] as Boolean
}
if (applicationInfo.metaData.containsKey(KEY_APPLICATION_ID)) {
SylinkConstants.APPLICATION_ID =
applicationInfo.metaData[KEY_APPLICATION_ID] as String
}
if (applicationInfo.metaData.containsKey(KEY_VERSION_NAME)) {
SylinkConstants.VERSION_CODE =
applicationInfo.metaData[KEY_VERSION_NAME] as String
}
}
} catch (e: PackageManager.NameNotFoundException) {
e.printStackTrace()
}
}
}至此,通过gradle.properties读取篇和本篇,能将项目中的properties配置在编译时成功写入
SylinkConstants
类中。