打包时为了在不同的变体中动态配置环境、常量等,可在模块的 build.gradle.kts 脚本中解析外部文件来实现,以下列举 2 种解析 json 的方式,将配置项动态添加到 BuildConfig.java 类中。
假设在 Windows 桌面有一个 config.json 文件("C:\\Users\\logcat\\Desktop\\config.json"),或者项目中某个文件夹下有个 config.json 文件("$rootDir/app/src/main/assets/config.json"):
{
"dev": {
"baseUrl": "http://1.2.3:8080/api/",
"isOpenLog": true
},
"prod": {
"baseUrl": "https://abc.com/api/",
"isOpenLog": false
},
"desc": {
"msg": "项目中的变体都需要在此声明,否则默认用 release 的配置。以上为 base 配置,以下配置跟项目中的 buildType 对应"
}
"debug": {
"parent": "dev",
"isOpenLog": true
},
"release": {
"parent": "prod"
},
"releaseWithLog": {
"parent": "prod",
"isOpenLog": true
}
}
首先在 application module(如 app)和 library module(如 base)的 gradle 脚本中支持 buildConfig,以便后续将配置项添加到 BuildConfig.java 文件中
android {
// ...
buildFeatures {
// ...
buildConfig = true
}
}
在 app module 或者 base module 中定义一下 2 种方法的公共模块,假设你在 base module 的 build.gradle.kts 中操作的:
android {
// ...
buildTypes {
// ...
}
}
fun checkFile(): File {
val path = "C:\\Users\\logcat\\Desktop\\config.json"/*"$rootDir/app/src/main/assets/config.json"*/
val jsonFile = file(path)
if (!jsonFile.exists()) {
throw GradleException("File not found: $path")
}
return jsonFile
}
data class BuildConfigField(
val type: String, val name: String, val value: Any?, val invoke: (Any?) -> String
)
方法一,通过反射实现继承,需要把 json 中的模型、字段名称在脚本中定义出来
继续在 base module 的 build.gradle.kts 中操作
import kotlin.reflect.full.memberProperties
plugins {
// ...
}
val jsonConfig: BuildConfigJson = Gson().fromJson(checkFile().readText(), BuildConfigJson::class.java)
android {
// ...
buildTypes {
debug {
// ...
wrapBuildConfigFields()
}
release {
// ...
wrapBuildConfigFields()
}
create("releaseWithLog") {
// ...
wrapBuildConfigFields()
}
}
}
data class BuildConfigJson(
val dev: BuildConfig,
val prod: BuildConfig,
val debug: BuildConfig?,
val release: BuildConfig?,
val releaseWithLog: BuildConfig?,
) {
data class BuildConfig(
val parent: String?,
val isOpenLog: Boolean,
val baseUrl: String
)
}
fun <T> KProperty1<T, *>.buildConfigField(t: T) = returnType.toString().run {
println("#configParams returnType:$this")
if (startsWith("kotlin.String")) {
BuildConfigField("String", name, invoke(t)) { "\"$it\"" }
} else {
BuildConfigField(
replace("kotlin.", "").replace("?", "").lowercase(), name, invoke(t)
) { "$it" }
}
}
fun com.android.build.api.dsl.BuildType.wrapBuildConfigFields() {
println("#wrapBuildConfigFields buildType-------->:${name}")
val self = (BuildConfigJson::class.memberProperties.find { it.name == name }
?.invoke(jsonConfig) as? BuildConfigJson.BuildConfig) ?: run {
println("================${name} is missing declared in config.json, use default: prod")
jsonConfig.prod
}
println("self: $self")
val parent = self.parent?.run {
BuildConfigJson::class.memberProperties.find { it.name == self.parent }
?.invoke(jsonConfig) as BuildConfigJson.BuildConfig
}
println("parent ${self.parent}: $parent")
BuildConfigJson.BuildConfig::class.memberProperties.mapNotNull {
it.buildConfigField(self).run self@{
value?.run { this@self } ?: parent?.run {
it.buildConfigField(parent).run parent@{
value?.run { this@parent } ?: run {
println("ignore buildConfigField: $type, $name, $value, ${invoke(value)}")
null
}
}
}
}
}.forEach {
println("#apply buildConfigField: ${it.type}, ${it.name},${it.value}, ${it.invoke(it.value)}")
buildConfigField(it.type, it.name, it.invoke(it.value))
}
}
方法二,通过Map实现继承,避免了在脚本中重复定义一次模型
继续在 base module 的 build.gradle.kts 中操作
val jsonConfig =
groovy.json.JsonSlurper().parse(checkFile()) as org.apache.groovy.json.internal.LazyMap
android {
// ...
buildTypes {
debug {
// ...
wrapBuildConfigFields()
}
release {
// ...
wrapBuildConfigFields()
}
create("releaseWithLog") {
// ...
wrapBuildConfigFields()
}
}
}
fun Map.Entry<String, Any>.buildConfigField() = when (value) {
is BigDecimal -> BuildConfigField("double", key, value) { "$it" }
is String -> BuildConfigField(value.javaClass.simpleName, key, value) { "\"${value}\"" }
else -> BuildConfigField(value.javaClass.simpleName, key, value) { "$value" }
}
fun com.android.build.api.dsl.BuildType.wrapBuildConfigFields() {
println("#wrapBuildConfigFields2 buildType-------->:${name}")
val self = (jsonConfig[name] ?: run {
println("================${name} is missing declared in config.json, use default: prod")
jsonConfig["release"] ?: throw GradleException("init config.json fail, missing release type")
}) as org.apache.groovy.json.internal.LazyMap
println("self: $self")
val parent = self["parent"]?.run { jsonConfig[this] as org.apache.groovy.json.internal.LazyMap }
println("parent ${self["parent"]}: $parent")
val mergeMap = mutableMapOf<String, Any>()
parent?.run { mergeMap.putAll(this) }
mergeMap.putAll(self)
mergeMap.entries.forEach { entry ->
entry.buildConfigField().run {
println("apply buildConfigField: ${type}, ${name},${value}, ${invoke(value)}")
buildConfigField(type, name, invoke(value))
}
}
}