Skip to content

Repository files navigation

kmp-build-logic

Shared Gradle convention plugins for Kotlin Multiplatform + Compose Multiplatform projects, write the module config once, apply it with one line everywhere.

CI No AI attribution Kotlin AGP Compose Multiplatform Plugins License

Why · Features · Architecture · Tech stack · Getting started · Roadmap

Portfolio: cv-siddharth.vercel.app  ·  Consumers: Doori  ·  PaymentsLab-KMP  ·  kmp-toolkit  ·  Candidai  ·  Gaddi  ·  kmp-app-template


Table of contents

At a glance, 17 convention plugins (KMP chain · Android · testing · quality · DI/data · flavors · Firebase · lint · purity) under one neutral shared.* prefix, :convention-only composite build, consumed today by 6 sibling repos: Doori, PaymentsLab-KMP, kmp-toolkit, Candidai, Gaddi and kmp-app-template.

Why this exists

Every KMP + Compose Multiplatform module ends up re-declaring the same boilerplate: apply Kotlin Multiplatform, apply the AGP KMP-library plugin, declare the iOS targets, wire the Compose compiler, pull in the same Koin/test/quality baseline for every module. Copy-pasting that across modules, or across repos, is exactly the kind of drift a convention plugin exists to kill.

This repo extracts that shared surface out of production KMP codebases into a standalone, independently-buildable Gradle composite build: 17 convention plugins under a neutral shared.* prefix, plus the Compose-compiler-metrics wiring several of them share. It's vendored as a git submodule (external/kmp-build-logic) and pulled in via pluginManagement { includeBuild(...) } by Doori, PaymentsLab-KMP, kmp-toolkit (currently at 2.0.0), Candidai, Gaddi and kmp-app-template, so the Kotlin/AGP/Compose/quality setup isn't hand-copied per project. Anything that genuinely diverges between consumers (app-specific flavor lists, repo-specific desktop/watchOS targets) was left out on purpose, see What's deliberately not here.

Features

Area Plugin ID Configures
KMP chain shared.kmp.library Kotlin Multiplatform + AGP KMP-library plugins, iosArm64() + iosSimulatorArm64()
shared.kmp.compose shared.kmp.library + Compose Multiplatform + Compose compiler plugins, Compose-metrics wiring
shared.cmp.feature shared.kmp.compose + the standard feature-module dep set (Compose runtime/UI/Material3, Koin, JetBrains navigation-compose, lifecycle-viewmodel, kotlinx-datetime)
shared.kmp.pure Kotlin Multiplatform only, jvm(), iosArm64(), iosSimulatorArm64(), wasmJs { browser(); nodejs() }, for platform-SDK-free leaf modules
Android shared.android.application AGP application + Compose-compiler plugins, compileSdk 37 / Java 21 / Compose enabled
shared.android.library AGP library + Compose-compiler plugins for an Android-only leaf module (e.g. kmp-toolkit's :security + its 11 payment-provider modules), compileSdk 37 / minSdk 24 / Java 21, single "release" variant with sources
shared.android.firebase Applies after shared.android.application, wires google-services + firebase-crashlytics Gradle plugins, the Firebase BOM/Analytics/Crashlytics runtime libs (looked up from the consumer's own catalog), and enables Crashlytics mapping-file upload for every build type
Testing shared.test JVM unit-test stack on testImplementation: JUnit, MockK, coroutines-test, Turbine, Koin-test
Quality shared.detekt Detekt 2.x static analysis, buildUponDefaultConfig, detekt-formatting ruleset
shared.ktlint ktlint Gradle plugin with defaults (alternative to shared.spotless)
shared.spotless Spotless with ktlint-based Kotlin/Kotlin-script formatting
shared.kover Kover coverage, root project configures filters/verify, every leaf self-registers into the aggregation
shared.android.lint Shared Android Lint config, reuses the app/library Lint extension when present (else applies standalone com.android.lint), enables XML + SARIF reports, checkDependencies, disables the GradleDependency nag
shared.purity Dependency-purity tripwire, a checkPurity task (wired into check) fails the build if any forbidden coordinate substring resolves on a configurable classpath (default jvmRuntimeClasspath); keeps a pure leaf module (engine/domain) honest without a full dependency-guard baseline
DI / data shared.koin Koin core DI wired into commonMain/commonTest for non-Compose modules
shared.room Room 3 (KMP) + KSP, schema export, runtime/compiler wired across Android + iOS targets
Flavors shared.flavors kmp-product-flavors (Android-style product flavors for KMP) with build-type support

Every plugin that applies the Compose compiler (shared.kmp.compose and transitively shared.cmp.feature, plus shared.android.application/shared.android.library directly) also picks up configureComposeCompilerMetrics(): it always wires the consumer's rootProject compose_stability.conf if present, and additionally emits Compose compiler metrics/stability reports under build/compose-metrics + build/compose-reports when run with -Pcompose.metrics. See compose_stability.conf in this repo for the template.

Architecture

graph LR
    subgraph chain["KMP chain — each builds on the one before it"]
        KL["shared.kmp.library"]
        KC["shared.kmp.compose"]
        CF["shared.cmp.feature"]
        KL --> KC --> CF
    end

    subgraph standalone["Independent plugins — no chain"]
        KP["shared.kmp.pure"]
        AA["shared.android.application"]
        AL["shared.android.library"]
        FB["shared.android.firebase"]
        T["shared.test"]
        DTK["shared.detekt"]
        KTL["shared.ktlint"]
        SPL["shared.spotless"]
        KOV["shared.kover"]
        LNT["shared.android.lint"]
        PUR["shared.purity"]
        KOI["shared.koin"]
        RM["shared.room"]
        FL["shared.flavors"]
        AA --> FB
    end
Loading

Engineering decisions

Decision Why Trade-off
Binary plugins (kotlin-dsl + explicit gradlePlugin { plugins { register(...) } }), not precompiled script plugins Real KDoc, an explicit apply(target: Project) body, and a plugin ID independent of the file name More boilerplate per plugin than a foo.gradle.kts auto-mapped ID
Version-catalog lookups inside plugin code go through VersionCatalogsExtension.findLibrary(...) reflectively, not the generated libs.xyz DSL Type-safe libs.foo accessors don't exist for a binary plugin class compiled before Gradle knows which project it applies to Alias typos surface at configuration time (NoSuchElementException), not compile time
Plugin-classpath deps (libs.android.gradlePlugin, etc.) are compileOnly, never implementation Avoids the same plugin class being loaded by two classloaders at two versions, that surfaces as a ClassCastException at apply-time, not a build-script error Every convention plugin author has to remember compileOnly
shared.kmp.library applies AGP's com.android.kotlin.multiplatform.library, not classic com.android.library AGP 9's purpose-built plugin for an Android target inside a kotlin { } block has multiplatform source-set awareness the classic plugin lacks As of AGP 9.4.0-alpha03 it has no assets-packaging support, shared.kmp.compose carries a documented workaround (configureComposeResourcesAndroidAssetsWorkaround) until upstream fixes it
gradle/libs.versions.toml is not re-declared in settings.gradle.kts The file already sits at Gradle's conventional path, so Gradle auto-registers it, an explicit versionCatalogs { create("libs") { from(...) } } block fails with "Multiple from invocations" Differs from consumer repos whose catalog lives one directory up, where the explicit block is required

Module map

Module Contents
:convention The only module in this composite build, 17 Plugin<Project> classes + ComposeMetrics.kt, registered via gradlePlugin { plugins { ... } } in convention/build.gradle.kts

Project structure

kmp-build-logic/
├── convention/
│   ├── build.gradle.kts          # kotlin-dsl + gradlePlugin{} registrations
│   └── src/main/kotlin/
│       ├── ComposeMetrics.kt                          # shared metrics/stability helper (not a plugin)
│       ├── SharedKmpLibraryConventionPlugin.kt
│       ├── SharedKmpComposeConventionPlugin.kt
│       ├── SharedCmpFeatureConventionPlugin.kt
│       ├── SharedKmpPureConventionPlugin.kt
│       ├── SharedAndroidApplicationConventionPlugin.kt
│       ├── SharedAndroidLibraryConventionPlugin.kt
│       ├── SharedAndroidApplicationFirebaseConventionPlugin.kt
│       ├── SharedAndroidLintConventionPlugin.kt
│       ├── SharedTestConventionPlugin.kt
│       ├── SharedDetektConventionPlugin.kt
│       ├── SharedKtlintConventionPlugin.kt
│       ├── SharedSpotlessConventionPlugin.kt
│       ├── SharedKoverConventionPlugin.kt
│       ├── SharedPurityConventionPlugin.kt
│       ├── SharedKoinConventionPlugin.kt
│       ├── SharedRoomConventionPlugin.kt
│       └── SharedFlavorsConventionPlugin.kt
├── gradle/libs.versions.toml     # plugin-classpath coordinates only (compileOnly)
├── compose_stability.conf        # template — copy into a consumer's root
└── settings.gradle.kts

Tech stack

Layer Version
Kotlin 2.4.20-RC
Android Gradle Plugin 9.5.0-alpha02
Compose Multiplatform 1.12.0-rc01
Gradle 9.7.0
Detekt 2.0.0-alpha.6
ktlint-gradle 14.2.0
Spotless 8.8.0
Kover 0.9.8
Room (KMP) 3.0.2
KSP 2.3.11
kmp-product-flavors 2.8.3
google-services 4.5.0
firebase-crashlytics (Gradle plugin) 3.0.8
Firebase BOM 34.18.0
JDK 21 (resolved automatically via the foojay toolchain resolver if not installed)

Getting started

Add this repo as a submodule and include it as a composite build:

git submodule add https://github.com/darkpandawarrior/kmp-build-logic.git external/kmp-build-logic
// settings.gradle.kts
pluginManagement {
    includeBuild("external/kmp-build-logic")
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
    }
}

Apply a plugin in any module's build file:

// core/data/build.gradle.kts
plugins {
    id("shared.kmp.library")
}

android {
    namespace = "com.example.core.data"
    compileSdk = 37
    defaultConfig { minSdk = 24 }
}

Plugins that look up dependencies from a version catalog (shared.cmp.feature, shared.test, shared.koin, shared.room, shared.android.firebase, shared.flavors) resolve those aliases from your own gradle/libs.versions.toml via VersionCatalogsExtension.findLibrary(...) see the Features table and each plugin's KDoc for the exact alias names it expects.

Building standalone

This repo is a self-contained composite build with no consumer project required:

git clone https://github.com/darkpandawarrior/kmp-build-logic.git
cd kmp-build-logic
./gradlew :convention:validatePlugins :convention:assemble

That's also the exact command CI runs (.github/workflows/ci.yml).

Roadmap

Shipped

  • KMP chain: shared.kmp.libraryshared.kmp.composeshared.cmp.feature, plus standalone shared.kmp.pure
  • Android application + Android-only library conventions
  • Quality stack: shared.detekt, shared.ktlint, shared.spotless, shared.kover, shared.android.lint
  • shared.purity, dependency-purity tripwire (checkPurity, wired into check) for pure leaf modules
  • DI/data: shared.koin, shared.room
  • shared.flavors (kmp-product-flavors integration)
  • shared.android.firebase, google-services + Crashlytics wiring, mapping-file upload
  • Compose compiler metrics/stability wiring shared across every Compose-applying plugin
  • CI: plugin validation + assemble, plus a commit-message AI-attribution guard
  • Grew from 2 to 6 consumers (Doori, PaymentsLab-KMP, kmp-toolkit, Candidai, Gaddi, kmp-app-template), including a non-app library consumer (kmp-toolkit) and a shared-vendoring pattern (kmp-toolkit itself pulled in as external/kmp-toolkit alongside external/kmp-build-logic)

Exploring

  • shared.android.library variants for consumers needing more than a single "release" variant
  • A shared.publishing convention once a consumer needs to publish artifacts beyond includeBuild

What's deliberately not here

  • AndroidProviderConventionPlugin, KmpLibraryWatchosConventionPlugin, KmpDesktopConventionPlugin repo-specific targets (a payment-provider module shape, watchOS, JVM desktop) out of scope for a shared surface arbitrary KMP projects would both want.
  • App-specific flavor dimensions/build types: shared.flavors wires the plugin, but the actual flavors are left to each consuming app, since the app consumers (Doori, PaymentsLab-KMP, Candidai) have divergent products and branding.

Portfolio: cv-siddharth.vercel.app  ·  Consumers: Doori  ·  PaymentsLab-KMP  ·  kmp-toolkit  ·  Candidai  ·  Gaddi  ·  kmp-app-template

About

Shared Gradle convention-plugin toolkit for Kotlin Multiplatform projects

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages