build.gradle.kts 解析外部 json 配置

打包时为了在不同的变体中动态配置环境、常量等,可在模块的 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))
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容