Home > database >  Kotlin multiplatform: Accessing build variables in code
Kotlin multiplatform: Accessing build variables in code

Time:12-13

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 (enter image description here

// ./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.

  1. I don't want to run this task manually. How can I make Gradle run it automatically?
  2. 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

  • Related