As a developer working on various Kotlin Multiplatform projects, whether for your job or just for fun, you might find that as your projects grow, you’ll want to create your own libraries. This is a common practice that helps you reuse the same code across different projects, saving time.
While this practice improves your code quality, over time, you might encounter dependency version conflicts, often referred to as “dependency hell”. Here’s an example to illustrate what can happen:
Imagine you are working on two different modules within a project, ModuleA
and ModuleB
, both depending on the same library, LibraryX
. However, ModuleA
requires LibraryX
version 1.2, while ModuleB
needs LibraryX
version 1.4 for its additional features.
When you try to build your project, the build system might fetch and use different versions of LibraryX
for each module, leading to inconsistent behavior or compilation errors. Worse, if both modules are part of the same build process, the build system might only resolve to one version of LibraryX
(say 1.2), causing ModuleB
to potentially fail at runtime due to missing features available only in 1.4.
With a BoM (Bill of Materials), you could centrally manage the version of LibraryX
, ensuring that all modules use a compatible version, or identify and resolve the version conflict upfront, deciding whether to upgrade ModuleA
to use LibraryX
1.4 or find a different solution that suits both modules. This centralized management helps maintain consistency, reduces the risk of runtime issues, and simplifies dependency updates and troubleshooting.
Here are some examples of famous libraries using a BoM:
Here’s a summary of the process explained in this article:
· The project structure
· The BoM configuration
· Add the BoM as a dependency in your projects
· Publish your BoM library to Maven Central via GitHub Actions
· Check your BoM library on Maven Central
⚠️ Important: I assume you already have a working setup to automatically publish a KMP library to Maven Central repository. If this is not the case, I recommend you read my article “How to publish your Kotlin Multiplatform library on Maven Central”:
The project structure
A few key elements are required to create a BoM:
- You need a multi-module project, where each module is a library.
- An additional module is dedicated to the BoM configuration. This module contains only one file (
build.gradle.kts
) and does not include any code, resources, or other files. (We will create it later on.)
➡️ By default, every module that sets up a MavenPublication
within that multi-module project is automatically included in the BoM.
Here’s what the project structure looks like:
myproject (root)
├── build.gradle.kts
├── bom (specific module for BoM configuration)
│ ├── build.gradle.kts
├── library1 (module)
│ ├── build.gradle.kts
│ ├── src
├── library2 (module)
│ ├── build.gradle.kts
│ ├── src
In this example, the BoM will include library1
and library2
.
For my kmp-bom project, with the default configuration, the BoM will include the following modules: kmp-common
, kmp-firebase
and kmp-realm
.
If you don’t know how to setup a multi-module project, follow the official Gradle documentation here: https://docs.gradle.org/current/userguide/multi_project_builds.html
The BoM configuration
A BoM is typically a Maven pom.xml file where all the libraries are listed along with their specific version numbers, providing a centralized reference for managing project dependencies:
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.test</groupId>
<artifactId>bom</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties>
<project1Version>1.0.0</project1Version>
<project2Version>1.0.0</project2Version>
</properties>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.test</groupId>
<artifactId>project1</artifactId>
<version>${project1Version}</version>
</dependency>
<dependency>
<groupId>com.test</groupId>
<artifactId>project2</artifactId>
<version>${project2Version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<modules>
<module>parent</module>
</modules>
</project>
Now, you could create this pom.xml
file and update it manually every time one of your libraries has a new version. However, we prefer to automate the process so that this pom.xml
file is automatically generated.
➡️ To do so, we use this Gradle plugin that does a pretty good job with minimum configuration:
- Configure the plugin for the
pom.xmlfile generation.
- Configure publishing to Maven Central and signing with GPG.
In your bom
module (the specific module for the BoM configuration), create a build.gradle.kts
file and add the following lines:
plugins { | |
id("io.github.gradlebom.generator-plugin").version("1.0.0.Final") | |
id("signing") | |
} | |
group = "io.github.tweener" // Change here | |
version = "1.0.0" // Change here | |
publishing { | |
publications { | |
create<MavenPublication>("Bom") { | |
artifactId = "kmp-bom" | |
pom { | |
name.set("My BoM SDK") // Change here | |
description.set("Bill of Materials (BoM) for my Kotlin Multiplatform libraries") // Change here | |
url.set("https://github.com/Tweener/kmp-bom") // Change here | |
licenses { | |
license { | |
name.set("The Apache License, Version 2.0") // Change here, if needed | |
url.set("http://www.apache.org/licenses/LICENSE-2.0.txt") // Change here, if needed | |
} | |
} | |
issueManagement { | |
system.set("GitHub Issues") | |
url.set("https://github.com/Tweener/kmp-bom/issues") // Change here | |
} | |
developers { | |
developer { | |
id.set("Tweener") // Change here | |
name.set("Vivien Mahé") // Change here | |
email.set("vivien@tweener-labs.com") // Change here | |
} | |
} | |
scm { | |
connection.set("scm:git:git://github.com:Tweener/kmp-bom.git") // Change here | |
developerConnection.set("scm:git:ssh://github.com:Tweener/kmp-bom.git")// Change here | |
url.set("https://github.com/Tweener/kmp-bom") // Change here | |
} | |
} | |
} | |
} | |
} | |
signing { | |
if (project.hasProperty("signing.gnupg.keyName")) { | |
println("Signing lib...") | |
useGpgCmd() | |
sign(publishing.publications) | |
} | |
} |
Job Offers
Exclude module from the BoM:
If you want to exclude a module from being collected into the BoM, you can add the excludeProject
configuration to your bom
module’s build.gradle.kts
:
bomGenerator {
excludeProject("excluded-module")
}
Include external dependencies to theBoM:
You can also include external dependencies (from outside this project) to your BoM, by adding the includeDependency
configuration to your bom
module’s build.gradle.kts
:
bomGenerator {
includeDependency("io.github.tweener:kmp-charts:")
}
name: Publish to Maven Central | |
permissions: | |
contents: read | |
on: | |
workflow_dispatch: | |
release: | |
types: [ released ] | |
jobs: | |
build-release: | |
uses: ./.github/workflows/buildRelease.yml | |
publish: | |
needs: build-release | |
env: | |
OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} | |
OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} | |
OSSRH_STAGING_PROFILE_ID: ${{ secrets.OSSRH_STAGING_PROFILE_ID }} | |
strategy: | |
matrix: | |
include: | |
- target: publishIosArm64PublicationToSonatypeRepository | |
os: macos-latest | |
- target: publishIosSimulatorArm64PublicationToSonatypeRepository | |
os: macos-latest | |
- target: publishIosX64PublicationToSonatypeRepository | |
os: macos-latest | |
- target: publishAndroidReleasePublicationToSonatypeRepository | |
os: ubuntu-latest | |
- target: publishKotlinMultiplatformPublicationToSonatypeRepository | |
os: ubuntu-latest | |
- target: publishBomPublicationToSonatypeRepository | |
os: ubuntu-latest | |
runs-on: ${{ matrix.os }} | |
steps: | |
- name: Checkout | |
uses: actions/checkout@v3 | |
- name: Validate Gradle Wrapper | |
uses: gradle/wrapper-validation-action@v1 | |
- name: Setup JDK 17 | |
uses: actions/setup-java@v3 | |
with: | |
java-version: '17' | |
distribution: "zulu" | |
- name: Setup Gradle cache | |
uses: actions/cache@v3 | |
with: | |
path: | | |
~/.konan | |
key: ${{ runner.os }}-${{ hashFiles('**/.lock') }} | |
- name: Import GPG key | |
uses: crazy-max/ghaction-import-gpg@v6 | |
with: | |
gpg_private_key: ${{ secrets.OSSRH_GPG_SECRET_KEY }} | |
passphrase: ${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} | |
- name: Gradle publish | |
uses: gradle/gradle-build-action@v3 | |
with: | |
arguments: | | |
${{ matrix.target }} | |
closeAndReleaseSonatypeStagingRepository | |
-Psigning.gnupg.passphrase=${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} | |
-Psigning.gnupg.keyName=${{ secrets.OSSRH_GPG_SECRET_KEY_ID }} | |
-PsonatypeUsername=${{ secrets.OSSRH_USERNAME }} | |
-PsonatypePassword=${{ secrets.OSSRH_PASSWORD }} | |
-PsonatypeStagingProfileId=${{ secrets.OSSRH_STAGING_PROFILE_ID }} |
Check your BoM library on Maven Central
With this, your library is now fully set up for automated publishing on Maven Central. After creating a release version of your library, it will be accessible via a URL provided by Sonatype.
For example, in my case, my BoM has 3 libraries: kmp-common
, kmp-firebase
, kmp-realm
. The URL is:
For a full example and source code, check out my kmp-bom repository:
https://github.com/Tweener/kmp-bom?source=post_page—–b8dd815aa018——————————–
That’s all for this article! I hope you found it helpful, I would greatly appreciate your feedback! 🙏
Feel free to leave a comment below or reach out to me directly:
- On X/Twitter: @VivienMahe
- On my website: vivienmahe.com
This article is previously published on proandroiddev.com