I'm working on a Kotlin Multiplatform project which is an SDK providing functionality for iOS & Android applications.
我正在做一个Kotlin多平台项目,这是一个为iOS和Android应用程序提供功能的SDK。
In our build.gradle.kts
we have a couple of variables that we would like to access within the common code shared code between iOS and Android.
在我们的Build.gradle.kts中,我们有几个变量,我们希望在iOS和Android之间共享的公共代码中访问它们。
As an Android developer this is how I would usually do in an Android project:
作为一名Android开发人员,我通常会在Android项目中这样做:
android {
...
defaultConfig {
...
buildConfigField "String", "SOME_VARIABLE", '"' + SOME_VARIABLE_IN_GRADLE_FILES + '"'
...
}
...
}
And then i could access it in code:
然后我可以用代码访问它:
val someVariable = BuildConfig.SOME_VARIABLE
How would one do to make something similar to work in a Kotlin Mulitplatform project, since BuildConfig
is not something that is recognised in the common shared code base.
如何在Kotlin多平台项目中做出类似的工作,因为在公共共享代码库中不能识别BuildConfig。
After searching on this topic for a solution I have yet not found any relevant answers, however my googlefoo skills might not be enough...
在这个话题上搜索了一个解决方案后,我还没有找到任何相关的答案,但我的谷歌技能可能还不够……
更多回答
The answer provided by @Evgeny will solve the problem, but I think this is a good example to learn how to solve this problem in pure Gradle. Even if you want to use a plugin, understanding how Gradle works can help with understand how to customise any plugin or Gradle behaviour.
@Evgeny提供的答案会解决这个问题,但我认为这是一个很好的例子,可以学习如何在纯Gradle中解决这个问题。即使你想使用插件,了解Gradle是如何工作的也有助于理解如何定制任何插件或Gradle行为。
Generating source code
There's a couple of Gradle utils that can be used to generate code from build properties.
有几个Gradle实用程序可用于从构建属性生成代码。
TextResourceFactory
can create files dynamically.
- Tasks that create output files can be used as inputs (example).
It's also important to be mindful of lazy configuration. The file should only be generated when it is necessary.
注意懒惰配置也很重要。该文件应仅在必要时生成。
BuildConfig generator task
Tasks are how work is defined in Gradle, so let's make a task that will produce a file as output. Sync
is a good task type for this. Sync
will copy any number of files into a directory.
任务是Gradle中定义工作的方式,所以让我们创建一个任务,该任务将生成一个文件作为输出。同步是一种很好的任务类型。同步会将任意数量的文件复制到一个目录中。
// build.gradle.kts
val buildConfigGenerator by tasks.registering(Sync::class) {
// from(...) // no input files yet
// the target directory
into(layout.buildDirectory.dir("generated-src/kotlin"))
}
If you run ./gradlew buildConfigGenerator
then the task will run, but the only thing that happens is that an empty directory ./build/generated-src/kotlin/
is created. So let's add a file as an input.
如果运行./gradlew BuildConfigGenerator,则任务将运行,但唯一发生的情况是创建了一个空目录./Build/Generated-src/kotlin/。因此,让我们添加一个文件作为输入。
Defining a generated file
TextResouceFactory
is a little known tool that can create text files dynamically. (Including downloading text files from URLs, which can come in handy!)
TextResouceFactory是一个鲜为人知的工具,可以动态创建文本文件。(包括从URL下载文本文件,这会派上用场!)
Let's create a Kotlin file using it.
让我们使用它创建一个Kotlin文件。
// build.gradle.kts
val buildConfigGenerator by tasks.registering(Sync::class) {
from(
resources.text.fromString(
"""
|package my.project
|
|object BuildConfig {
| const val PROJECT_VERSION = "${project.version}"
|}
|
""".trimMargin()
)
) {
rename { "BuildConfig.kt" } // set the file name
into("my/project/") // change the directory to match the package
}
into(layout.buildDirectory.dir("generated-src/kotlin/"))
}
Note that I had to rename the file (TextResourceFactory
will generate a random name), and put set the directory to match package my.project
.
请注意,我必须重命名该文件(TextResourceFactory将生成一个随机名称),并设置目录以匹配包my.project。
Now if you run ./gradlew buildConfigGenerator
it will actually generate a file!
现在如果你跑。gradlew buildConfigGenerator它实际上会生成一个文件!
// ./build/generated-src/kotlin/BuildConfig.kt
package my.project
object BuildConfig {
const val PROJECT_VERSION = "0.0.1"
}
However, there are two more things to fix.
然而,还有两件事需要解决。
- I don't want to run this task manually. How can I make Gradle run it automatically?
- Gradle doesn't recognise
./build/generated-src/kotlin/
as a source directory.
We can fix both in one go!
我们可以一气呵成地解决这两个问题!
Linking the task to the source set
You can add new source directories to a Kotlin SourceSet via the Kotlin Multiplatform plugin DSL via kotlin.srcDir()
您可以通过kotlin.srcDir()通过Kotlin多平台插件DSL向Kotlin SourceSet添加新的源目录。
// build.gradle.kts
plugins {
kotlin("multiplatform") version "1.7.22"
}
kotlin {
sourceSets {
val commonMain by getting {
kotlin.srcDir(/* add the generate source directory here... */)
}
}
}
We could hard-code kotlin.srcDir(layout.buildDirectory.dir("generated-src/kotlin/"))
- but now we haven't told Gradle about our task! We would still have to run it manually.
我们可以硬编码kotlin.srcDir(layout.buildDirectory.dir(“generated-src/kotlin/”))--但现在我们还没有告诉Gradle我们的任务!我们仍然必须手动运行它。
Fortunately, we can have the best of both worlds. Thanks to Gradle's Provider API, a task can be converted into a file-provider.
幸运的是,我们可以两全其美。由于Gradle的提供程序API,任务可以转换为文件提供程序。
// build.gradle.kts
kotlin {
sourceSets {
val commonMain by getting {
kotlin.srcDir(
// convert the task to a file-provider
buildConfigGenerator.map { it.destinationDir }
)
}
}
}
(Note that because buildConfigGenerator
was created using tasks.registering()
its type is TaskProvider
, the same won't be true of tasks created using tasks.creating()
.)
(请注意,因为BuildConfigGenerator是使用tasks.Register()创建的,所以它的类型是TaskProvider,所以使用tasks.Creating()创建的任务就不是这样了。)
generatedSourceDirProvider
will lazily provide the generated directory, and because it was mapped from a task, Gradle knows that it needs to run the connected task whenever the commonMain
source set is used for compilation.
GeneratedSourceDirProvider将延迟地提供生成的目录,并且因为它是从任务映射而来的,所以Gradle知道每当使用CommonMain源码集进行编译时,它都需要运行连接的任务。
To test this, run ./gradlew clean
. The build directory is gone, including the generated file. Now run ./gradlew assemble
- and Gradle will automatically run generatedSourceDirProvider
. Magic!
要测试这一点,请运行./gradlew lean。构建目录已经消失,包括生成的文件。现在运行./gradlew ASSEMBLE-Gradle将自动运行GeneratedSourceDirProvider。魔术!
Future improvements
Generating the source during IDEA sync
It's a bit annoying that the source isn't generated when I first open the project in IntelliJ. I could add the gradle-idea-ext-plugin, and make IntelliJ trigger buildConfigGenerator
on a Gradle sync.
当我第一次在IntelliJ中打开项目时,没有生成源代码,这有点恼人。我可以添加Gradle-Idea-ext-plugin,并使IntelliJ触发器在Gradle同步上构建ConfigGenerator。
Dynamic property
In this example, I hard-coded project.version
. But what if the project version is dynamic?
在本例中,我硬编码了项目.版本。但是,如果项目版本是动态的呢?
In this case, we need to use a provider, which can be mapped to a file.
在这种情况下,我们需要使用提供程序,该提供程序可以映射到文件。
// build.gradle.kts
val buildConfigGenerator by tasks.registering(Sync::class) {
// create a provider for the project version
val projectVersionProvider: Provider<String> = provider { project.version.toString() }
// map the project version into a file
val buildConfigFileContents: Provider<TextResource> =
projectVersionProvider.map { version ->
resources.text.fromString(
"""
|package my.project
|
|object BuildConfig {
| const val PROJECT_VERSION = "$version"
|}
|
""".trimMargin()
)
}
// Gradle accepts file providers as Sync inputs
from(buildConfigFileContents) {
rename { "BuildConfig.kt" }
into("my/project/")
}
into(layout.buildDirectory.dir("generated-src/kotlin/"))
}
Dynamic properties
What if you want to generate a file from multiple properties? In this case, I would either
如果您想从多个属性生成一个文件,该怎么办?在这种情况下,我会选择
You can use com.github.gmazzo.buildconfig
gradle plugin for that purpose (github).
为此,您可以使用com.giighb.gmazzo.Buildconfig Gradle插件(GitHub)。
You can find sample for KMM project inside.
你可以在里面找到KMM项目的样本。
Basically you should add:
基本上,您应该添加:
plugins {
kotlin("multiplatform")
// other plugins
id("com.github.gmazzo.buildconfig") version "<latest>"
}
// ...
buildConfig {
// common config
buildConfigField("String", "COMMON_VALUE", "\"aCommonValue\"")
// for specific source set
sourceSets.named<BuildConfigSourceSet>("jvmMain") {
buildConfigField("String", "PLATFORM", "\"jvm\"")
buildConfigField("String", "JVM_VALUE", "\"JvmValue\"")
}
}
Also you can take a look at com.codingfeline.buildkonfig
(github) it has similar API
您还可以查看com.codingFeline.Buildkonfig(GitHub),它有类似的API
you can use open source library for making build konfig fields and it's too easy to use
https://github.com/yshrsmz/BuildKonfig
你可以使用开源库来制作构建Konfig域,而且使用https://github.com/yshrsmz/BuildKonfig太容易了
please make sure when u add library plugin u also must specify the buildKonfig block
请确保当您添加库插件时,您还必须指定BuildKonfig块
import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.0")
classpath("com.codingfeline.buildkonfig:buildkonfig-gradle-plugin:latest_version")
}
}
plugins {
kotlin("multiplatform")
id("com.codingfeline.buildkonfig")
}
kotlin {
// your target config...
android()
iosX64('ios')
}
buildkonfig {
packageName = "com.example.app"
// objectName = "YourAwesomeConfig"
// exposeObjectWithName = "YourAwesomePublicConfig"
defaultConfigs {
buildConfigField(STRING, "name", "value")
}
}
更多回答
Thank you @aSemy, could not have asked for a more clear answer. Worked like a charm!
谢谢你@aSemy,再也没有比这更明确的答案了。真是妙不可言!
@aSemy Your answer helped me a lot. By the way, I used the Copy
task because I was concerned about the following statement. > All files that exist in the destination directory will be deleted before copying files docs.gradle.org/current/dsl/org.gradle.api.tasks.Sync.html
@aSemy你的回答对我帮助很大。顺便说一句,我使用复制任务是因为我担心下面的语句。>在复制文件docs.gradle.org/current/dsl/org.gradle.api.tasks.Sync.html之前,将删除目标目录中存在的所有文件
@wrongwrong Typically I'd recommend using Sync over Copy, because if the name of a file changes then Copy won't clean up the destination directory, meaning the destination is dirty, while Sync will remove outdated files. However, if you're certain the file name will never change, or you want to confirm the behaviour first then Copy is an alternative.
@错误错误通常我建议使用同步而不是复制,因为如果文件名更改,复制将不会清理目标目录,这意味着目标是脏的,而同步将删除过时的文件。但是,如果您确定文件名永远不会更改,或者您想要首先确认行为,则可以选择复制。
我是一名优秀的程序员,十分优秀!