Setelah memahami pengujian KMP, kita siap membangun library untuk dipublikasi. Library KMP berbeda dari library JVM biasa—satu kode sumber menghasilkan banyak artifact: library-jvm.jar, library-android.aar, library-iosarm64.klib, library-js.ir, dan library-metadata.jar. Pengguna library hanya perlu mendeklarasikan satu dependency di build.gradle.kts mereka, dan Gradle akan otomatis memilih artifact yang sesuai dengan target mereka. Pada chapter ini kita akan membedah struktur library KMP, melihat konfigurasi build.gradle.kts dengan plugin maven-publish, dan memahami proses publikasi ke Maven Central.

Struktur Library KMP #

Library KMP mengikuti pola source set yang sama dengan aplikasi: commonMain berisi kode bersama, source set spesifik berisi kode platform. Perbedaan utama: library juga menghasilkan artifact metadata yang berisi deklarasi commonMain—digunakan konsumen untuk resolusi target. Tanpa artifact metadata, Gradle tidak dapat menemukan artifact jvm atau iosArm64 saat konsumen menambahkan dependency.

text
my-library/
├── build.gradle.kts
├── settings.gradle.kts
└── src/
    ├── commonMain/kotlin/com/example/Util.kt
    ├── commonTest/kotlin/com/example/UtilTest.kt
    ├── jvmMain/kotlin/com/example/Platform.jvm.kt
    ├── androidMain/kotlin/com/example/Platform.android.kt
    ├── iosMain/kotlin/com/example/Platform.ios.kt
    └── jsMain/kotlin/com/example/Platform.js.kt

Deklarasi Target dan Plugin #

File build.gradle.kts untuk library KMP memerlukan dua plugin: kotlin("multiplatform") untuk kompilasi lintas platform, dan maven-publish untuk publikasi artifact. Plugin signing ditambahkan bila ingin mempublikasi ke Maven Central yang mensyaratkan signature GPG.

kotlin build.gradle.kts: deklarasi library KMP
plugins {
    kotlin("multiplatform") version "2.0.21"
    `maven-publish`
    signing
}

group = "id.example.belajar"
version = "1.0.0"

kotlin {
    androidTarget { publishLibraryVariants("release") }
    iosX64()
    iosArm64()
    iosSimulatorArm64()
    jvm()
    js { browser(); nodejs() }
    linuxX64()
}

Setiap target menghasilkan artifact dengan format berbeda—jvm() menghasilkan JAR, androidTarget() menghasilkan AAR, iosX64() menghasilkan framework atau KLIB. Konsumen library cukup menambahkan satu baris dependency, dan Gradle akan otomatis memilih artifact sesuai target mereka.

Simulasi Publikasi Library #

Demo berikut mensimulasikan struktur library KMP yang dipublikasi ke Maven Central dan beberapa repository lain. Model data menggambarkan target publikasi, source set, dan metadata POM yang dibutuhkan.

kotlin
// kotlin_multiplatform_library.kt
// Membangun dan mempublikasi library KMP untuk Maven Central.
// Demo: simulasi struktur library + target artifacts + metadata publikasi.

data class TargetPublikasi(
    val name: String,
    val artifactId: String,
    val outputType: String,
    val sinkronisasiKe: String
)

data class LibraryMetadata(
    val groupId: String,
    val artifactId: String,
    val version: String,
    val license: String,
    val scmUrl: String,
    val developer: String
)

data class SourceSetArtifact(
    val sourceSet: String,
    val artifactName: String,
    val ukuranKB: Int,
    val jumlahKotlinFiles: Int
)

class PublikasiLibrary(
    private val metadata: LibraryMetadata,
    private val targets: List<TargetPublikasi>,
    private val sourceSets: List<SourceSetArtifact>
) {
    fun gambarStruktur(): String {
        val sb = StringBuilder()
        sb.appendLine("Library: ${metadata.groupId}:${metadata.artifactId}:${metadata.version}")
        sb.appendLine("License: ${metadata.license}")
        sb.appendLine("SCM    : ${metadata.scmUrl}")
        sb.appendLine("Author : ${metadata.developer}")
        sb.appendLine()
        sb.appendLine("Source sets:")
        for (s in sourceSets) {
            sb.appendLine("  ${s.sourceSet.padEnd(22)} -> ${s.artifactName} (${s.ukuranKB} KB, ${s.jumlahKotlinFiles} file)")
        }
        sb.appendLine()
        sb.appendLine("Target artifacts yang dipublikasi:")
        for (t in targets) {
            sb.appendLine("  ${t.name.padEnd(20)} artifact=${t.artifactId.padEnd(40)} type=${t.outputType}")
        }
        return sb.toString().trimEnd()
    }

    fun gambarGradleKts(): String {
        return """
            plugins {
                kotlin("multiplatform") version "2.0.21"
                `maven-publish`
                signing
            }

            group = "${metadata.groupId}"
            version = "${metadata.version}"

            kotlin {
                androidTarget { publishLibraryVariants("release") }
                iosX64()
                iosArm64()
                iosSimulatorArm64()
                jvm()
                js { browser(); nodejs() }
                linuxX64()

                sourceSets {
                    val commonMain by getting
                    val commonTest by getting
                }
            }

            publishing {
                publications.withType<MavenPublication> {
                    pom {
                        name.set("${metadata.artifactId}")
                        description.set("Library KMP untuk demonstrasi publikasi")
                        url.set("${metadata.scmUrl}")
                        licenses {
                            license {
                                name.set("${metadata.license}")
                            }
                        }
                        developers {
                            developer { id.set("${metadata.developer}") }
                        }
                        scm {
                            url.set("${metadata.scmUrl}")
                        }
                    }
                }
            }
        """.trimIndent()
    }

    fun verifikasiSinkronisasi(): List<Pair<String, Boolean>> {
        return targets.map { t ->
            t.name to (t.sinkronisasiKe.isNotBlank())
        }
    }
}

fun main() {
    val meta = LibraryMetadata(
        groupId = "id.example.belajar",
        artifactId = "kmp-util",
        version = "1.0.0",
        license = "Apache-2.0",
        scmUrl = "https://github.com/example/kmp-util",
        developer = "budi"
    )

    val targets = listOf(
        TargetPublikasi("jvm",       "kmp-util-jvm",        "JAR",       "Maven Central"),
        TargetPublikasi("android",   "kmp-util-android",    "AAR",       "Maven Central"),
        TargetPublikasi("iosArm64",  "kmp-util-iosarm64",  "Framework", "Maven Central + CocoaPods"),
        TargetPublikasi("iosX64",    "kmp-util-iosx64",     "Framework", "Maven Central + CocoaPods"),
        TargetPublikasi("iosSimArm", "kmp-util-iossimulatorarm64", "Framework", "Maven Central + CocoaPods"),
        TargetPublikasi("js",        "kmp-util-js",         "NPM package", "npm registry"),
        TargetPublikasi("linuxX64",  "kmp-util-linuxx64",   "KLIB",       "Maven Central"),
        TargetPublikasi("metadata",  "kmp-util-metadata",   "KLIB (common)", "Maven Central")
    )

    val sourceSets = listOf(
        SourceSetArtifact("commonMain",            "kmp-util-metadata",        18, 4),
        SourceSetArtifact("commonTest",            "kmp-util-metadata",         6, 2),
        SourceSetArtifact("jvmMain",               "kmp-util-jvm",             12, 2),
        SourceSetArtifact("androidMain",           "kmp-util-android",         10, 2),
        SourceSetArtifact("iosMain",               "kmp-util-iosarm64",         8, 2),
        SourceSetArtifact("iosX64Main",            "kmp-util-iosx64",            8, 1),
        SourceSetArtifact("iosArm64Main",          "kmp-util-iosarm64",          8, 1),
        SourceSetArtifact("iosSimulatorArm64Main", "kmp-util-iossimulatorarm64", 8, 1),
        SourceSetArtifact("jsMain",                "kmp-util-js",                7, 1),
        SourceSetArtifact("linuxX64Main",          "kmp-util-linuxx64",          7, 1)
    )

    val lib = PublikasiLibrary(meta, targets, sourceSets)

    println(lib.gambarStruktur())
    println()
    println("=== Ringkasan publikasi ===")
    println("Jumlah artifact publikasi : ${targets.size}")
    println("Jumlah source set         : ${sourceSets.size}")
    println("Total ukuran source       : ${sourceSets.sumOf { it.ukuranKB }} KB")
    println()

    println("=== Verifikasi sinkronisasi ke repository ===")
    for ((name, sinkron) in lib.verifikasiSinkronisasi()) {
        val status = if (sinkron) "siap publikasi" else "PERLU SETUP"
        println("  ${name.padEnd(20)} : $status")
    }
    println()

    println("=== Konfigurasi build.gradle.kts (ringkas) ===")
    println(lib.gambarGradleKts())
}

Output yang Dihasilkan:

text
Library: id.example.belajar:kmp-util:1.0.0
License: Apache-2.0
SCM    : https://github.com/example/kmp-util
Author : budi

Source sets:
  commonMain             -> kmp-util-metadata (18 KB, 4 file)
  commonTest             -> kmp-util-metadata (6 KB, 2 file)
  jvmMain                -> kmp-util-jvm (12 KB, 2 file)
  androidMain            -> kmp-util-android (10 KB, 2 file)
  iosMain                -> kmp-util-iosarm64 (8 KB, 2 file)
  iosX64Main             -> kmp-util-iosx64 (8 KB, 1 file)
  iosArm64Main           -> kmp-util-iosarm64 (8 KB, 1 file)
  iosSimulatorArm64Main  -> kmp-util-iossimulatorarm64 (8 KB, 1 file)
  jsMain                 -> kmp-util-js (7 KB, 1 file)
  linuxX64Main           -> kmp-util-linuxx64 (7 KB, 1 file)

Target artifacts yang dipublikasi:
  jvm                  artifact=kmp-util-jvm                             type=JAR
  android              artifact=kmp-util-android                         type=AAR
  iosArm64             artifact=kmp-util-iosarm64                        type=Framework
  iosX64               artifact=kmp-util-iosx64                          type=Framework
  iosSimArm            artifact=kmp-util-iossimulatorarm64               type=Framework
  js                   artifact=kmp-util-js                              type=NPM package
  linuxX64             artifact=kmp-util-linuxx64                        type=KLIB
  metadata             artifact=kmp-util-metadata                        type=KLIB (common)

=== Ringkasan publikasi ===
Jumlah artifact publikasi : 8
Jumlah source set         : 10
Total ukuran source       : 92 KB

=== Verifikasi sinkronisasi ke repository ===
  jvm                  : siap publikasi
  android              : siap publikasi
  iosArm64             : siap publikasi
  iosX64               : siap publikasi
  iosSimArm            : siap publikasi
  js                   : siap publikasi
  linuxX64             : siap publikasi
  metadata             : siap publikasi

=== Konfigurasi build.gradle.kts (ringkas) ===
plugins {
    kotlin("multiplatform") version "2.0.21"
    `maven-publish`
    signing
}

group = "id.example.belajar"
version = "1.0.0"

kotlin {
    androidTarget { publishLibraryVariants("release") }
    iosX64()
    iosArm64()
    iosSimulatorArm64()
    jvm()
    js { browser(); nodejs() }
    linuxX64()

    sourceSets {
        val commonMain by getting
        val commonTest by getting
    }
}

publishing {
    publications.withType<MavenPublication> {
        pom {
            name.set("kmp-util")
            description.set("Library KMP untuk demonstrasi publikasi")
            url.set("https://github.com/example/kmp-util")
            licenses {
                license {
                    name.set("Apache-2.0")
                }
            }
            developers {
                developer { id.set("budi") }
            }
            scm {
                url.set("https://github.com/example/kmp-util")
            }
        }
    }
}

Mari kita bedah bagian kunci dari kode di atas:

  1. LibraryMetadata: metadata POM yang wajib untuk Maven Central—groupId, artifactId, version, license, scmUrl, dan developer. Maven Central menolak artifact tanpa informasi ini.
  2. TargetPublikasi: model untuk satu target publikasi—nama target, artifact ID, jenis output (JAR/AAR/Framework/KLIB), dan repository tujuan.
  3. SourceSetArtifact: model untuk satu source set dengan nama artifact dan ukuran. Pada proyek nyata, ukuran dihitung setelah build—demo ini memakai nilai statis.
  4. PublikasiLibrary.gambarStruktur(): mencetak ringkasan library—metadata, source set, target artifacts. Format ini menggambarkan apa yang akan dilihat pengguna di halaman Maven Central.
  5. PublikasiLibrary.gambarGradleKts(): menghasilkan konfigurasi build.gradle.kts yang setara dengan demo. Perhatikan blok publishing { publications.withType<MavenPublication> { pom { ... } } } yang menambahkan metadata POM.
  6. verifikasiSinkronisasi(): daftar status per target—siap publikasi atau perlu setup. Bila satu target belum dikonfigurasi repository tujuan, library belum dapat dirilis.
  7. Output menunjukkan 8 artifact: jvm, android, tiga iosArm64/iosX64/iosSimulatorArm64, js, linuxX64, dan metadata. Artifact metadata berisi kode commonMain yang dipakai Gradle untuk resolusi target.

Persyaratan Maven Central #

Maven Central mensyaratkan beberapa hal yang tidak ada di repository lokal. Pertama, artifact harus di-sign dengan GPG—plugin signing melakukan ini otomatis bila signing.keyId dan signing.password di-set di ~/.gradle/gradle.properties. Kedua, POM wajib berisi name, description, url, license, developers, dan scm. Ketiga, perlu akun Sonatype OSSRH dengan namespace yang sesuai (mis. id.example.belajar untuk domain belajar.example.id).

kotlin build.gradle.kts: signing dan POM
signing {
    useInMemoryPgpKeys(
        findProperty("signingKey") as String?,
        findProperty("signingPassword") as String?
    )
    sign(publishing.publications)
}

Referensi Lanjutan #

Penutup #

Membangun library KMP untuk Maven Central memerlukan konfigurasi yang lebih kompleks daripada library JVM biasa—multiple target, artifact metadata, dan signature GPG. Namun, bagi konsumen library, penggunaan tetap sederhana: satu baris dependency, Gradle yang memilih artifact. Chapter berikutnya akan masuk ke topik konkret pertama: networking dengan ktor-client, bagaimana satu kode HTTP dapat berjalan di JVM, Android, dan iOS.