背景

公司项目目前安卓原生端有三个项目,分别是APP、智能电视看板、智能手表终端

三个应用在业务端有不同的表现,但在底层架构(基础组件)上应用一套逻辑

所以目前的架构需求是:

  1. 抽离基础组件上传到maven,以依赖的方式集成到各个项目

  2. 在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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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')
}

android {
defaultConfig {
if (SYLINK_IS_BUILD_MODULE.toBoolean() && !SYLINK_IS_JENKINS.toBoolean()) {
applicationId "com.sylink.app.dfs.xxx"
}
}
}

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
2
3
<activity
android:name="com.sylink.app.dfs.fixture.FixtureMainActivity"
tools:node="remove" />

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的基类

image-20211028173156669

由于主进程已经有默认定义的ContentProvider,但子进程没有,所以需要自定义一个MultipleProcessStartupProvider作为子进程的ContentProvider

由于组件之间有初始化顺序要求,所以定义一个BaseInit,后续其他模块的Init类必须在这个类初始化完之后进行,FrameInitDelegate为基础初始化代理类,

初始化一些所有组件都需要使用的工具类,如MMKV,Timber等

BaseInit如下:

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
class BaseInit : AndroidStartup<Unit>() {

//是否在主线程调用
override fun callCreateOnMainThread(): Boolean {
return true
}

override fun create(context: Context) {
FrameInitDelegate(context as Application)
startKoin {
logger(KoinLogger())
androidContext(context)
fragmentFactory()

}
Timber.d("Base模块初始化,注入KoinApplication")
}


//是否阻塞主线程
override fun waitOnMainThread(): Boolean {
return true
}


}

BaseRemoteInit如下:

由于是在子进程运行,我们需要加入@MultipleProcess(":remote")注解来定义开启进程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@MultipleProcess(":remote")
class BaseRemoteInit : AndroidStartup<Unit>() {
override fun callCreateOnMainThread(): Boolean {
return false
}

override fun create(context: Context) {
RemoteInitDelegate(context as Application)
startKoin {
logger(KoinLogger())
androidContext(context)
}
Timber.d("BaseRemote模块初始化,注入KoinApplication")
}


override fun waitOnMainThread(): Boolean {
return false
}


}

最后不要忘了在xml中定义配置类和子进程的ContentProvider

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
<provider
android:name="com.rousetime.android_startup.provider.StartupProvider"
android:authorities="${SYLINK_APPLICATION_ID}.android_startup"
android:exported="false">

<meta-data
android:name="com.sylink.dfs.app.event.init.main.BaseInit"
android:value="android.startup" />


<meta-data
android:name="com.sylink.dfs.app.event.init.main.provider.StartupConfigProvider"
android:value="android.startup.provider.config" />

</provider>

<provider
android:name="com.sylink.dfs.app.event.init.remote.provider.MultipleProcessStartupProvider"
android:authorities="${SYLINK_APPLICATION_ID}.android_startup.remote"
android:exported="false"
android:process=":remote">

<meta-data
android:name="com.sylink.dfs.app.event.init.remote.BaseRemoteInit"
android:value="android.startup" />

<meta-data
android:name="com.sylink.dfs.app.event.init.remote.provider.RemoteStartupConfigProvider"
android:value="android.startup.provider.config" />
</provider>

在其他模块中使用时,必须实现dependencies接口,定义NetworkInit的onCreate方法在BaseInit初始化完成后进行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class NetworkInit : AndroidStartup<Unit>() {
override fun callCreateOnMainThread(): Boolean {
return true
}

override fun create(context: Context) {
loadKoinModules(listOf(networkAppModule))
}

override fun waitOnMainThread(): Boolean {
return false
}

override fun dependencies(): List<Class<out Startup<*>>> {
return listOf(BaseInit::class.java)
}

}

通过android-startup我们就实现了组件间的初始化代码隔离,真正的做到了可插拔组件

Activity

概念

页面跳转不能使用原始的Intent形式,因为Intent形式会显示声明Activity::class.java

核心

使用路由的方式进行页面的跳转

实现

使用了DRouter 来实现页面间的路由跳转

在项目的build.gradle中加入

1
2
3
dependencies {        
classpath "io.github.didi:drouter-plugin-proxy:1.0.1"
}

在组件的build.gradle中加入

1
api "io.github.didi:drouter-api:2.1.3"

在Activity上加入路由

1
2
@Router(path = HomeRoutePath.home_activity_path)
class HomeActivity : BaseActivity() {

跳转方法

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

  1. BaseInit中调用startKoin方法,KoinLogger是实现的Koin日志打印自定义类,androidContext是给Koin注入上下文,fragmentFactory是谷歌新推出的fragment有参构造方法的Fragment生产工厂,如果有使用到可具体了解,这边不再赘述。

    1
    2
    3
    4
    5
    startKoin {
    logger(KoinLogger())
    androidContext(context)
    fragmentFactory()
    }
  2. 在各个模块中定义di包,其中放module类,新建xxxModule类存放普通的实例(如各个模块的ApiService),

    新建xxxViewModelModule.kt类,存放viewModel类,

    get<>()方法能从其他已经被注入的Module中获取实例,

    用single定义的实例为单例

    用viewModel定义的类可在后面注入时自动绑定activity / fragment的生命周期

    用factory定义的类每次注入时都会创建一个新的单例

    1
    2
    3
    4
    5
    6
    7
    val storageModule = module {

    single<StorageApiService> {
    get<Retrofit>().create(StorageApiService::class.java)
    }

    }

    下面的viewModel中的一个构造参数是StorageApiService,就可以通过get()方法获得上面定义的单例实例

    1
    2
    3
    4
    5
    6
    7
    8
    val storageViewModelModule = module {
    viewModel {
    StorageOutInViewModel(get(),get())
    }
    viewModel {
    StoragePickViewModel(get(),get())
    }
    }
  3. 在各个模块的Init类的onCreate方法中,调用loadKoinModules方法,参数为本模块的module数组

    1
    2
    3
    4
    5
    6
    7
    class StorageInit : BaseAndroidStartup() {

    override fun create(context: Context) {
    loadKoinModules(listOf(storageViewModelModule, storageFragmentModule, storageModule))
    }

    }
  4. 在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插件进行上传

  1. 新建一个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" //快照版本
  2. 定义maven库的快照和release的URL

    1
    2
    def 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/"
  3. 编写上传方法,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
    29
    afterEvaluate {
    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
    }
    }
    }
    }
  4. 编写上传源码的方法,这样在业务组件中可以点击查看基础组件的源码,而不是编译后的.class文件,方便调试

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    task 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'
    }
  5. 当assets中有资源时,需编写上传assets的方法

    1
    2
    3
    4
    5
    6
    task assetsJar(type:Jar) {
    classifier = "assets"
    android.sourceSets.all { sourceSet ->
    from sourceSet.assets.srcDirs
    }
    }
  6. 当包中有aar或jar包依赖时,我们需要把这两个本地包一起打包,这边使用 fat-aar-android这个gradle插件可帮我们将aar和jar包打包进自己的aar包中

    在项目的build.gradle中依赖fat-aar插件

    1
    2
    3
    dependencies {
    classpath 'com.github.kezong:fat-aar:1.3.6'
    }

    在组件的build.gradle中应用插件,并将需要打包的aar或jar包使用embed替代implementation

    1
    2
    3
    4
    5
    6
    7
    8
    apply 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')
    }
  7. 经过上述的配置后,在AndroidStudio右侧的Gradle配置中会有一个publishing的任务组,点击publish即可上传至maven,点击publishToMavenLocal即可上传到本地maven仓库

    image-20211029173520891

  8. 每个项目都有依赖仓库,App使用公司的maven仓库,所以需要在Repositories中加入公司maven仓库的地址

  9. 由于使用的是快照版本,gradle对SNAPSHOT版本存在缓存,会导致拉取快照依赖的时候并不是maven上最新的版本,

    需要在每个业务模块的build.gradle中加入配置:

    1
    2
    3
    4
    //快照版本永远检查最新
    project.configurations.all {
    resolutionStrategy.cacheChangingModulesFor 0, 'seconds'
    }
  10. 经过上述操作后,最终我们依赖如下:

    1
    2
    3
    dependencies {
    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仓库,使得所有模块可使用

实现

新建插件

  1. 在Android Studio中创建一个项目,并新建一个module,module类型选择 Java or Kotlin Library,并填写相关信息

    image-20211103162740664

  2. 新建完module后,在src/main下新建groovy文件夹,在groovy文件夹下新建包名文件夹,在src/main下新建resources文件夹,并在其下新建META-INF文件夹,再新建gradle-plugins文件夹,最终创建文件,包名.properties,包名必须与上面的groovy文件夹下的包名一致,如图:

    image-20211103163117596

  3. 在module的build.gradle文件中apply groovy插件和我们自己编写的上传至maven的push.gradle插件,并依赖gradleApi()和localGroovy()

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    plugins {
    id 'groovy'
    }

    apply from: '../push.gradle'

    dependencies {
    implementation gradleApi()
    implementation localGroovy()
    }
  4. 在包名文件夹下新建XXXConrtoller.groovy文件,并实现Plugin<Project>,当项目apply了本插件并执行sync project时,就会自动执行apply中的逻辑

    1
    2
    3
    4
    5
    6
    7
    8
    class DependenciesVersionController implements Plugin<Project> {

    @Override
    void apply(Project project) {
    //TODO
    }

    }
  5. 在包名.properties文件中,申明插件的Controller

    1
    implementation-class=com.sylink.dfs.app.version.plugin.DependenciesVersionController
  6. 至此前期准备工作全部结束,接下来就是插件逻辑的编写

plugin统一定义

  1. 使用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
    19
    class 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')

    }
    }
  2. 在XXXController的apply方法中调用addPlugins方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class DependenciesVersionController implements Plugin<Project> {

    @Override
    void apply(Project project) {
    SylinkProperties properties = PropertiesUtils.readProperties(project)

    AppPlugins.addPlugins(project, properties)
    }
    }

build配置统一管理

  1. 定义AppInfoExt.java,写入android版本常量

    1
    2
    3
    4
    5
    6
    7
    public 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";

    }
  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
    53
    class 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()
    }

    }
    }
    }
  3. 在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
    }

    }
    }
  4. 最后在XXXController的apply方法中调用addConfig方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    class DependenciesVersionController implements Plugin<Project> {

    @Override
    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
2
3
4
5
6
7
8
static boolean isReleaseBuildType(Gradle gradle) {
for (String s : gradle.getStartParameter().getTaskNames()) {
if (s.toLowerCase().contains("release")) {
return true
}
}
return false
}

maven仓库统一配置

maven的配置也可在插件中统一设置

  1. 新建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
    28
    class 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,这在云端组件的调试阶段非常有用。

  2. 在XXXController的apply方法中调用addRepos方法,并且在上传至Maven篇中获得最新快照版本的配置也可统一写入这里

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class DependenciesVersionController implements Plugin<Project> {

    @Override
    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)
    }

    }

依赖统一管理

  1. 新建Versions.java,用来定义依赖版本号

    1
    2
    3
    4
    5
    6
    7
    public 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 用到的依赖版本
    }
  2. 根据依赖分组建立依赖路径管理包,如Room下有四个依赖路径,新建Room.java文件

    1
    2
    3
    4
    5
    6
    public 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;
    }
  3. 根据不同组件建议依赖管理java文件,并定义依赖数组,如Event组件,新建EventExt.java文件,定义eventDepList数组,数组中存放的是 步骤2. 中的依赖路径

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public 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,

    };
    }
  4. 在插件中新建ModuleEnum,放入各个组件的名字,方便判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public 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;
    }
    }
  5. 新建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
    27
    class 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
    }
    }
    }
    }
  6. 在XXXController的apply方法中调用addDep方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    class DependenciesVersionController implements Plugin<Project> {
    @Override
    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)

    }
    }

使用

  1. 新建项目篇中已经介绍了插件集成了push.gradle,跟云端组件上传至Maven一样,在插件开发完成后点publish即可发布到Maven仓库中

    需要注意的是这个gradle插件,不是android的项目,所以上传来源有所不同。

    在android项目中,上传来源是components.release,而gradle插件则是components.java

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    afterEvaluate {
    publishing {
    publications {
    Production(MavenPublication) {
    // from components.release 这是android组件的配置
    from components.java

    //TODO ...
    }
    }
    }
    }
  2. 在项目的build.gradle文件中添加

    1
    2
    3
    dependencies {
    classpath 'com.shuyilink:dfs-app-version:1.0.0-XXXXXXX'
    }
  3. 在模块的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插件中

  1. 在插件中定义PropertiesEnum,来定义gradle.properties中值的key

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    enum 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,
    }
  2. 定义bean类SylinkProperties,将上面的值定义在bean类中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public 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方法

    }
  3. 编写方法读取项目中properties中的值并赋值到SylinkProperties中,properties中值的key必须跟PropertiesEnum中一样

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    class 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 其他项赋值
    }
    }
  4. 在XXXController的apply方法中调用readProperties方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    class DependenciesVersionController implements Plugin<Project> {

    @Override
    void apply(Project project) {

    SylinkProperties properties = PropertiesUtils.readProperties(project)
    println properties

    }

    }
  5. 至此,只要在组件中依赖gradle插件,就能统一读取gradle.properties中的属性并通过properties实例使用

BuildConfig失效

问题

上传至maven的是aar包,aar包可以理解为加了资源文件的jar包,jar包中是编译后的java/kotlin文件,即.class文件。

这也是为什么只有云端依赖的组件化才能真正提高编译速度的原因

而BuildConfig是在编译中根据build.gradle的buildTypes中的配置项写入的。

既然aar包已经编译了,那云端组件的BuildConfig自然也就已经写入生成了,所以在云端组件中BuildConfig.DEBUG永远是false

需要寻找可替代BuildConfig的配置,使云端依赖也能跟随实际项目的配置而改变,比如服务器地址,UI自适应的宽高等

这个配置需满足两个条件:

  1. 能同时影响本地组件和编译后的云端组件(能被这两种组件共用)
  2. 在编译时根据build.gradle文件动态改变对应的值

实现

使用AndroidManifest.xml替代Build.Config,

对于上面两个条件

  1. 在编译时所有模块的AndroidManifest.xml会合并成一个文件,满足被所有组件共用的条件
  2. AndroidManifest.xml中的属性可以在build.gradle中通过manifestPlaceholders设置,且可通过反射被读出,满足动态改变的条件。

同样使用自定义的插件来实现所有模块的配置统一读写方法

  1. 动态配置属性均写在gradle.properties中,使用上面的gradle.properties读取篇,将配置写入sylinkProperties实例

  2. 根据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,
    ]
    }
    }
    }
  3. 在项目壳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}" />

  4. 在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
    53
    object 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()
    }
    }
    }
  5. 至此,通过gradle.properties读取篇和本篇,能将项目中的properties配置在编译时成功写入SylinkConstants类中。