I'm working on a Kotlin Multiplatform project which is an SDK providing functionality for iOS & Android applications.
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.
As an Android developer this is how I would usually do in an Android project:
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.
After searching on this topic for a solution I have yet not found any relevant answers, however my googlefoo skills might not be enough...
CodePudding user response:
You can use com.github.gmazzo.buildconfig
gradle plugin for that purpose (
// ./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()
// 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.
Fortunately, we can have the best of both worlds. Thanks to Gradle's Provider API, a task can be converted into a file-provider.
// 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()
.)
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.
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!
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.
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
- create a custom task type, with a named input property for each property I want to use to generate the file
- or use a
MapProperty
, set all properties into it, and then (becauseMapProperty
is a Gradle provider).map { }
the map into a file