Skip to content
lang-kotlin-library-dev logo

Kotlin Library Development

lang-kotlin-library-dev

Kotlin-specific library development patterns. Use when creating Kotlin libraries, designing idiomatic Kotlin APIs with extension functions and DSLs, configuring Gradle Kotlin DSL (build.gradle.kts), managing multiplatform projects, testing with Kotest/JUnit, writing KDoc documentation, or publish...

SKILL.md

Full skill instructions

Kotlin Library Development

Kotlin-specific patterns for library development. This skill extends meta-library-dev with Kotlin tooling, multiplatform capabilities, and ecosystem practices.

This Skill Extends

  • meta-library-dev - Foundational library patterns (API design, versioning, testing strategies)

For general concepts like semantic versioning, module organization principles, and testing pyramids, see the meta-skill first.

This Skill Adds

  • Kotlin tooling: Gradle Kotlin DSL, multiplatform configuration, compiler plugins
  • Kotlin idioms: Extension functions, DSL design, sealed classes, inline functions
  • Kotlin ecosystem: Maven Central publishing, KDoc, Dokka, multiplatform targets

This Skill Does NOT Cover

  • General library patterns - see meta-library-dev
  • Android-specific library development - see Android skills
  • Kotlin/​JS frontend development - see frontend skills
  • General Kotlin syntax - see lang-kotlin-dev

Quick Reference

TaskCommand/​Pattern
New librarygradle init --type kotlin-library
Build./​gradlew build
Test./​gradlew test
Generate docs./​gradlew dokkaHtml
Publish (local)./​gradlew publishToMavenLocal
Publish (Maven Central)./​gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository
Check API compatibility./​gradlew apiCheck (requires binary-compatibility-validator)

Gradle Kotlin DSL Structure

build.gradle.kts (JVM Library)

plugins {
    kotlin("jvm") version "1.9.22"
    id("org.jetbrains.dokka") version "1.9.20"
    `maven-publish`
    signing
}

group = "com.example"
version = "1.0.0"

repositories {
    mavenCentral()
}

kotlin {
    jvmToolchain(17)

    compilerOptions {
        freeCompilerArgs.add("-Xjsr305=strict")
        allWarningsAsErrors.set(true)
    }
}

dependencies {
    implementation(kotlin("stdlib"))

    testImplementation(kotlin("test"))
    testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
    testImplementation("io.kotest:kotest-assertions-core:5.8.0")
}

tasks.test {
    useJUnitPlatform()
}

java {
    withJavadocJar()
    withSourcesJar()
}

publishing {
    publications {
        create<MavenPublication>("maven") {
            from(components["java"])

            pom {
                name.set("My Kotlin Library")
                description.set("A brief description of what this library does")
                url.set("https://github.com/​username/​repo")

                licenses {
                    license {
                        name.set("The Apache License, Version 2.0")
                        url.set("http://www.apache.org/​licenses/​LICENSE-2.0.txt")
                    }
                }

                developers {
                    developer {
                        id.set("username")
                        name.set("Your Name")
                        email.set("[email protected]")
                    }
                }

                scm {
                    connection.set("scm:git:git://github.com/​username/​repo.git")
                    developerConnection.set("scm:git:ssh://github.com/​username/​repo.git")
                    url.set("https://github.com/​username/​repo")
                }
            }
        }
    }

    repositories {
        maven {
            name = "sonatype"
            url = uri("https://s01.oss.sonatype.org/​service/​local/​staging/​deploy/​maven2/")
            credentials {
                username = project.findProperty("sonatypeUsername") as String?
                password = project.findProperty("sonatypePassword") as String?
            }
        }
    }
}

signing {
    sign(publishing.publications["maven"])
}

gradle.properties

# Publishing configuration
signing.keyId=ABCD1234
signing.password=your-gpg-password
signing.secretKeyRingFile=/​path/​to/​secring.gpg

sonatypeUsername=your-username
sonatypePassword=your-password

# Kotlin configuration
kotlin.code.style=official
kotlin.parallel.tasks.in.project=true

# Build optimizations
org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m
org.gradle.parallel=true
org.gradle.caching=true

Multiplatform Configuration

build.gradle.kts (Multiplatform)

plugins {
    kotlin("multiplatform") version "1.9.22"
    id("org.jetbrains.dokka") version "1.9.20"
    `maven-publish`
}

group = "com.example"
version = "1.0.0"

kotlin {
    // JVM target
    jvm {
        compilations.all {
            kotlinOptions.jvmTarget = "17"
        }
        testRuns["test"].executionTask.configure {
            useJUnitPlatform()
        }
    }

    // JavaScript target
    js(IR) {
        browser()
        nodejs()
    }

    // Native targets
    linuxX64()
    macosX64()
    macosArm64()
    mingwX64()

    // iOS targets
    iosX64()
    iosArm64()
    iosSimulatorArm64()

    sourceSets {
        // Common source set
        val commonMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
            }
        }

        val commonTest by getting {
            dependencies {
                implementation(kotlin("test"))
            }
        }

        // JVM-specific
        val jvmMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.3")
            }
        }

        val jvmTest by getting

        // JS-specific
        val jsMain by getting
        val jsTest by getting

        // Native shared
        val nativeMain by creating {
            dependsOn(commonMain)
        }

        val nativeTest by creating {
            dependsOn(commonTest)
        }

        // Configure all native targets
        val linuxX64Main by getting { dependsOn(nativeMain) }
        val macosX64Main by getting { dependsOn(nativeMain) }
        val macosArm64Main by getting { dependsOn(nativeMain) }
        val mingwX64Main by getting { dependsOn(nativeMain) }

        val iosX64Main by getting { dependsOn(nativeMain) }
        val iosArm64Main by getting { dependsOn(nativeMain) }
        val iosSimulatorArm64Main by getting { dependsOn(nativeMain) }
    }
}

Hierarchical Source Set Structure

src/
├── commonMain/​kotlin/
│   └── com/​example/
│       └── Library.kt
├── commonTest/​kotlin/
│   └── com/​example/
│       └── LibraryTest.kt
├── jvmMain/​kotlin/
│   └── com/​example/
│       └── JvmSpecific.kt
├── jvmTest/​kotlin/
├── jsMain/​kotlin/
├── jsTest/​kotlin/
├── nativeMain/​kotlin/
│   └── com/​example/
│       └── NativeSpecific.kt
└── nativeTest/​kotlin/

Idiomatic Kotlin API Design

Extension Functions

Use extension functions for fluent APIs:

// Good: Extension function pattern
fun String.toSnakeCase(): String =
    this.replace(Regex("([a-z])([A-Z])"), "$1_$2").lowercase()

// Usage
val result = "HelloWorld".toSnakeCase() // "hello_world"

// Good: Extension on generic types
fun <T> List<T>.second(): T? = this.getOrNull(1)

// Good: Extension with receiver
inline fun <T> T.apply(block: T.() -> Unit): T {
    block()
    return this
}

DSL Design

Type-safe builders for configuration:

// DSL API
class HttpClient internal constructor() {
    var timeout: Long = 30_000
    var followRedirects: Boolean = true
    val headers: MutableMap<String, String> = mutableMapOf()

    fun header(name: String, value: String) {
        headers[name] = value
    }
}

// Builder function
fun httpClient(configure: HttpClient.() -> Unit): HttpClient {
    return HttpClient().apply(configure)
}

// Usage
val client = httpClient {
    timeout = 60_000
    followRedirects = false
    header("User-Agent", "MyApp/​1.0")
}

Nested DSL example:

@DslMarker
annotation class HtmlDsl

@HtmlDsl
abstract class Tag(val name: String) {
    private val children = mutableListOf<Tag>()

    protected fun <T : Tag> initTag(tag: T, init: T.() -> Unit): T {
        tag.init()
        children.add(tag)
        return tag
    }
}

class HTML : Tag("html") {
    fun head(init: Head.() -> Unit) = initTag(Head(), init)
    fun body(init: Body.() -> Unit) = initTag(Body(), init)
}

class Head : Tag("head") {
    fun title(init: Title.() -> Unit) = initTag(Title(), init)
}

class Title : Tag("title")
class Body : Tag("body")

// Usage
fun html(init: HTML.() -> Unit): HTML = HTML().apply(init)

val page = html {
    head {
        title { }
    }
    body { }
}

Sealed Classes for Type Safety

Use sealed classes for restricted hierarchies:

// Good: Sealed class for results
sealed interface Result<out T> {
    data class Success<T>(val value: T) : Result<T>
    data class Failure(val error: Throwable) : Result<Nothing>
}

// Exhaustive when expressions
fun <T> Result<T>.getOrThrow(): T = when (this) {
    is Result.Success -> value
    is Result.Failure -> throw error
}

// Good: Sealed class for events
sealed class NetworkEvent {
    data class Connected(val connectionId: String) : NetworkEvent()
    data class Disconnected(val reason: String) : NetworkEvent()
    data class MessageReceived(val message: String) : NetworkEvent()
    data object Reconnecting : NetworkEvent()
}

Inline Functions for Performance

Use inline for higher-order functions:

// Good: Inline function with reified type
inline fun <reified T> Any.asOrNull(): T? = this as? T

// Usage without explicit type
val string: String? = someObject.asOrNull()

// Good: Inline with lambda
inline fun <T> measureTime(block: () -> T): Pair<T, Long> {
    val start = System.currentTimeMillis()
    val result = block()
    val time = System.currentTimeMillis() - start
    return result to time
}

// Good: Inline value classes for type safety
@JvmInline
value class UserId(val value: String)

@JvmInline
value class Email(val value: String)

// No runtime overhead, but type-safe
fun sendEmail(userId: UserId, email: Email) { }

Operator Overloading

Use operator functions judiciously:

data class Vector(val x: Double, val y: Double) {
    operator fun plus(other: Vector) = Vector(x + other.x, y + other.y)
    operator fun minus(other: Vector) = Vector(x - other.x, y - other.y)
    operator fun times(scalar: Double) = Vector(x * scalar, y * scalar)
    operator fun unaryMinus() = Vector(-x, -y)
}

// Usage
val v1 = Vector(1.0, 2.0)
val v2 = Vector(3.0, 4.0)
val sum = v1 + v2
val scaled = v1 * 2.0

Module Organization

Standard Library Structure

my-kotlin-library/
├── build.gradle.kts
├── gradle.properties
├── settings.gradle.kts
├── src/
│   ├── main/​kotlin/
│   │   └── com/​example/​library/
│   │       ├── Library.kt           # Public API
│   │       ├── Models.kt            # Data classes
│   │       ├── Extensions.kt        # Extension functions
│   │       ├── Dsl.kt              # DSL builders
│   │       └── internal/           # Internal implementation
│   │           └── Utils.kt
│   └── test/​kotlin/
│       └── com/​example/​library/
│           ├── LibraryTest.kt
│           └── DslTest.kt
├── docs/
│   └── index.md
└── README.md

Public API Organization

Main library file (Library.kt):

package com.example.library

// Public API exports
public fun createClient(): Client = ClientImpl()

public interface Client {
    fun connect(): Result<Unit>
    fun disconnect()
}

// Internal implementation
internal class ClientImpl : Client {
    override fun connect(): Result<Unit> = TODO()
    override fun disconnect() = TODO()
}

Visibility Modifiers

// Public - part of API (default, but explicit is clearer)
public fun publicFunction() {}

// Internal - visible within module only
internal fun internalFunction() {}

// Private - visible within file only
private fun privateFunction() {}

// Protected - visible in class and subclasses
protected open class Base {
    protected fun protectedFunction() {}
}

Testing with Kotest

Test Structure

import io.kotest.core.spec.style.FunSpec
import io.kotest.core.spec.style.StringSpec
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.shouldNotBe
import io.kotest.matchers.collections.shouldContain
import io.kotest.matchers.string.shouldStartWith

// String spec (simple)
class SimpleTest : StringSpec({
    "string length should be 5" {
        "hello".length shouldBe 5
    }

    "string should start with h" {
        "hello" shouldStartWith "h"
    }
})

// Fun spec (descriptive)
class CalculatorTest : FunSpec({
    test("addition should work correctly") {
        val result = 2 + 2
        result shouldBe 4
    }

    context("division") {
        test("dividing by non-zero should work") {
            10 / 2 shouldBe 5
        }

        test("dividing by zero should throw") {
            shouldThrow<ArithmeticException> {
                10 / 0
            }
        }
    }
})

// Behavior spec (BDD)
class ClientTest : BehaviorSpec({
    given("a connected client") {
        val client = createClient()
        client.connect()

        `when`("sending a message") {
            val result = client.send("test")

            then("it should succeed") {
                result shouldBe Success
            }
        }

        `when`("disconnecting") {
            client.disconnect()

            then("it should be disconnected") {
                client.isConnected shouldBe false
            }
        }
    }
})

Property-Based Testing

import io.kotest.core.spec.style.FunSpec
import io.kotest.property.Arb
import io.kotest.property.arbitrary.*
import io.kotest.property.checkAll

class PropertyTest : FunSpec({
    test("string reverse is involutive") {
        checkAll<String> { str ->
            str.reversed().reversed() shouldBe str
        }
    }

    test("addition is commutative") {
        checkAll(Arb.int(), Arb.int()) { a, b ->
            a + b shouldBe b + a
        }
    }

    test("custom generator") {
        val emailArb = arbitrary { rs ->
            val name = Arb.string(5..10).bind()
            val domain = Arb.string(5..10).bind()
            "$name@$domain.com"
        }

        checkAll(emailArb) { email ->
            email shouldContain "@"
        }
    }
})

JUnit Integration

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.ValueSource
import kotlin.test.assertEquals
import kotlin.test.assertTrue

class JUnitTest {
    private lateinit var client: Client

    @BeforeEach
    fun setup() {
        client = createClient()
    }

    @Test
    @DisplayName("Client should connect successfully")
    fun `client should connect`() {
        val result = client.connect()
        assertTrue(result.isSuccess)
    }

    @ParameterizedTest
    @ValueSource(strings = ["hello", "world", "test"])
    fun `should handle various inputs`(input: String) {
        val result = process(input)
        assertTrue(result.isNotEmpty())
    }
}

KDoc Documentation

Documentation Style

/​**
 * Creates an HTTP client with the specified configuration.
 *
 * This function uses a DSL to configure the client. All settings
 * are optional and have sensible defaults.
 *
 * @param configure A lambda with receiver for configuring the client
 * @return A configured HTTP client instance
 *
 * @sample com.example.samples.basicClientExample
 * @see HttpClient for available configuration options
 * @since 1.0.0
 *
 * Example:
 * ```kotlin
 * val client = httpClient {
 *     timeout = 60_000
 *     followRedirects = false
 *     header("User-Agent", "MyApp/​1.0")
 * }
 * ```
 */
fun httpClient(configure: HttpClient.() -> Unit): HttpClient {
    return HttpClient().apply(configure)
}

/​**
 * Represents the result of an operation.
 *
 * @param T The type of the successful result value
 * @property value The result value if successful
 * @property error The error if failed
 *
 * @constructor Creates a result instance
 */
sealed class Result<out T> {
    /​**
     * Represents a successful result.
     *
     * @property value The successful value
     */
    data class Success<T>(val value: T) : Result<T>()

    /​**
     * Represents a failed result.
     *
     * @property error The error that caused the failure
     */
    data class Failure(val error: Throwable) : Result<Nothing>()
}

Dokka Configuration

build.gradle.kts:

plugins {
    id("org.jetbrains.dokka") version "1.9.20"
}

tasks.dokkaHtml {
    outputDirectory.set(buildDir.resolve("dokka"))

    dokkaSourceSets {
        named("main") {
            moduleName.set("My Library")
            includes.from("docs/​index.md")

            sourceLink {
                localDirectory.set(file("src/​main/​kotlin"))
                remoteUrl.set(
                    URL("https://github.com/​user/​repo/​tree/​main/​src/​main/​kotlin")
                )
                remoteLineSuffix.set("#L")
            }

            externalDocumentationLink {
                url.set(URL("https://kotlinlang.org/​api/​latest/​jvm/​stdlib/"))
            }
        }
    }
}

Publishing to Maven Central

Setup Requirements

  1. Sonatype OSSRH Account: Sign up at https://issues.sonatype.org
  2. GPG Key: Generate and publish a GPG key
  3. Gradle Configuration: Configure publishing plugin

GPG Key Setup

# Generate key
gpg --gen-key

# List keys
gpg --list-keys

# Export public key
gpg --keyserver keyserver.ubuntu.com --send-keys YOUR_KEY_ID

# Export private key
gpg --export-secret-keys YOUR_KEY_ID > secring.gpg

Publishing Plugin Configuration

build.gradle.kts:

plugins {
    `maven-publish`
    signing
    id("io.github.gradle-nexus.publish-plugin") version "1.3.0"
}

nexusPublishing {
    repositories {
        sonatype {
            nexusUrl.set(uri("https://s01.oss.sonatype.org/​service/​local/"))
            snapshotRepositoryUrl.set(
                uri("https://s01.oss.sonatype.org/​content/​repositories/​snapshots/")
            )
            username.set(project.findProperty("sonatypeUsername") as String?)
            password.set(project.findProperty("sonatypePassword") as String?)
        }
    }
}

publishing {
    publications {
        create<MavenPublication>("maven") {
            from(components["java"])

            artifact(tasks["kotlinSourcesJar"])
            artifact(tasks["dokkaJavadocJar"])

            pom {
                name.set(project.name)
                description.set("Library description")
                url.set("https://github.com/​user/​repo")

                licenses {
                    license {
                        name.set("The Apache License, Version 2.0")
                        url.set("http://www.apache.org/​licenses/​LICENSE-2.0.txt")
                    }
                }

                developers {
                    developer {
                        id.set("userid")
                        name.set("User Name")
                        email.set("[email protected]")
                    }
                }

                scm {
                    connection.set("scm:git:git://github.com/​user/​repo.git")
                    developerConnection.set("scm:git:ssh://github.com/​user/​repo.git")
                    url.set("https://github.com/​user/​repo")
                }
            }
        }
    }
}

signing {
    sign(publishing.publications["maven"])
}

val dokkaJavadocJar by tasks.registering(Jar::class) {
    dependsOn(tasks.dokkaJavadoc)
    from(tasks.dokkaJavadoc.flatMap { it.outputDirectory })
    archiveClassifier.set("javadoc")
}

val kotlinSourcesJar by tasks.registering(Jar::class) {
    from(sourceSets["main"].allSource)
    archiveClassifier.set("sources")
}

Publishing Workflow

# 1. Build the project
./​gradlew build

# 2. Publish to local Maven for testing
./​gradlew publishToMavenLocal

# 3. Publish to Sonatype staging
./​gradlew publishToSonatype

# 4. Close and release staging repository
./​gradlew closeAndReleaseSonatypeStagingRepository

# Or all in one
./​gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository

Binary Compatibility

API Validation Plugin

build.gradle.kts:

plugins {
    id("org.jetbrains.kotlinx.binary-compatibility-validator") version "0.14.0"
}

apiValidation {
    ignoredProjects.add("test-utils")

    nonPublicMarkers.add("com.example.InternalApi")

    validationDisabled = false
}

Usage

# Generate API dump
./​gradlew apiDump

# Check API compatibility
./​gradlew apiCheck

Commit the API dump:

my-library/
├── api/
│   └── my-library.api

Anti-Patterns

1. Overusing Extension Functions

// Bad: Extension on Any
fun Any.doSomething() { }

// Good: Specific type
fun String.doSomething() { }

2. Breaking API with Default Parameters

// v1.0.0
fun connect(host: String, port: Int = 8080)

// v1.1.0 - WRONG! This is breaking
fun connect(host: String, port: Int = 8080, timeout: Long)

// v1.1.0 - Correct: Add overload or new default at end
fun connect(host: String, port: Int = 8080, timeout: Long = 30000)

3. Not Using Visibility Modifiers

// Bad: Everything public by default
class InternalHelper {
    fun helpfulMethod() { }
}

// Good: Explicit visibility
internal class InternalHelper {
    internal fun helpfulMethod() { }
}

4. Mutable Public Collections

// Bad: Mutable public property
val items: MutableList<String> = mutableListOf()

// Good: Immutable public interface
private val _items: MutableList<String> = mutableListOf()
val items: List<String> get() = _items

// Or use read-only collection
val items: List<String> = listOf()

Common Dependencies

Coroutines

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
    testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.3")
}

Serialization

plugins {
    kotlin("plugin.serialization") version "1.9.22"
}

dependencies {
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}

Testing

dependencies {
    testImplementation(kotlin("test"))
    testImplementation("io.kotest:kotest-runner-junit5:5.8.0")
    testImplementation("io.kotest:kotest-assertions-core:5.8.0")
    testImplementation("io.kotest:kotest-property:5.8.0")
    testImplementation("io.mockk:mockk:1.13.9")
}

References

More skills from majiayu000

xiaohongshu logo
majiayu000/claude-arsenal

xiaohongshu

xiaohongshu

286 148
View
agent-task-conductor logo
majiayu000/claude-skill-registry

agent-task-conductor

Conduct multi-agent task orchestration and workflow coordination.

663 1
View
conductor-setup logo
majiayu000/claude-skill-registry

conductor-setup

Initialize project with Conductor artifacts (product definition,

663 1
View
animation-designer logo
majiayu000/claude-skill-registry

animation-designer

Expert in web animations, transitions, and motion design using Framer Motion and CSS

663 1
View
diagramming logo
majiayu000/claude-skill-registry

diagramming

Creates Mermaid and ASCII diagrams for flowcharts, architecture, ERDs, state machines, mindmaps, and more. Use when user mentions diagram, flowchart, mermaid, ASCII diagram, text diagram, terminal diagram, visualize, C4, mindmap, architecture diagram, sequence diagram, ERD, or needs visual docume...

663 1
View
h3-pg logo
majiayu000/claude-skill-registry-data

h3-pg

PostgreSQL bindings for H3 hexagonal grid system. Use when working with H3 cells in Postgres, including spatial indexing, geometry/geography integration, and raster analysis.

23 1
View
conductor-development logo
majiayu000/claude-skill-registry

conductor-development

Context-Driven Development skill for projects using Conductor. Use this skill when you detect a `conductor/` directory in the project, when working on tasks defined in a `plan.md` file, or when the user asks about tracks, specs, or plans. Automatically applies TDD workflow, tracks task completion...

663 1
View
conductor-status logo
majiayu000/claude-skill-registry

conductor-status

Display project status, active tracks, and next actions

663 1
View
dockerization logo
majiayu000/claude-skill-registry

dockerization

Official Stakpak application containerization standard operating procedure, a step-by-step guidline to properly dockerize applications. This is a rule book curated by the Stakpak Team.

663 1
View

Popular AI tools

Kaiber logo
Video

Kaiber

Generate, edit, and beat-sync AI video with leading models in one workspace.

Paid
View
Vimcal logo
Productivity

Vimcal

The world's fastest calendar for remote work

Free
View

Transform Your Design with AI Designer by ImgCreator.ai

Freemium
View
Akool AI logo
Content & writing

Akool AI

Revolutionizing Video Production with AI-Powered Creativity

Paid
View

Extend an image past the frame and let AI fill the new aspect ratio.

Freemium
View
StarByFace logo
Security

StarByFace

Discover your celebrity doppelgänger with StarByFace!

Free
View
C

ChainClarity explains 700+ crypto whitepapers in plain English, with layered summaries, comparisons, research tools, alerts, and a $4.99 Pro plan.

Freemium
View
Opus Clip logo
Coding & apps

Opus Clip

Opus.ai: Revolutionize Your Web Experience

Free
View