Upgrade to Pro — share decks privately, control downloads, hide ads and more …

How to Publish Your First Kotlin Multiplatform Library

How to Publish Your First Kotlin Multiplatform Library

Publishing a Kotlin multiplatform library can be very challenging. This presentation will walk through the whole process from drafting a concept to the publication of your first multiplatform library.

We will be using KStore - a minimal multiplatform library to store objects to disk - as a case study for this presentation. We will explore how it was originally inspired, the original goals and how its API evolved over time to support multiple platforms.

A key goal of this side-project was to make it self-managed as much as possible with minimal overhead so that the open-source community can extend or maintain the library even in my absence. To achieve this I've incorporated a number of tools to upkeep standards from code contributions - tools such as Klover code coverage and binary-compatibility-validator. I've also used a number of automation tools to streamline the process of publishing a multiplatform library - tools such as GitHub actions build matrices to build and test on multiple platforms.

Isuru Rajapakse

April 18, 2023
Tweet

Transcript

  1. Top 10 Essential Tools YOU NEED! to Publish Your Own

    Multiplatform Library! Introduction
  2. interface ValueStore<T> { fun get(): Maybe<T> fun observePut(value: T): Single<T>

    fun observe(): Observable<ValueUpdate<T>?> fun observeClear(): Completable class ValueUpdate<T>(val value: T?) } RxStore Incubate > Inspire
  3. class KStore<T : @Serializable Any> { suspend fun get(): T?

    suspend fun put(value: T) val updates: Flow<T?> suspend fun clear() } KStore Incubate > Inspire
  4. @RequiresOptIn(message = "This API is experimental”) @Retention(BINARY) @Target(CLASS, FUNCTION,PROPERTY) annotation

    class ExperimentalMyLibraryApi feature-1 feature-2 utils MyLibrary YourProject YourLibrary Incubate > Isolate https: // github.com/Kotlin/api-guidelines ExperimentalMyLibraryApi
  5. @ExperimentalMyLibraryApi fun interface NewApi { fun doSomethingNew() = TODO() }

    feature-1 feature-2 utils MyLibrary YourProject YourLibrary Incubate > Isolate https: // github.com/Kotlin/api-guidelines ExperimentalMyLibraryApi @OptIn(ExperimentalMyLibraryApi :: class) val myApi = NewApi { . . }
  6. name: Build on: .. jobs: check: .. build: .. release:

    if: ${{ github.event_name != 'pull_request' }} needs: build publish: if: ${{ github.event_name != 'pull_request' }} needs: build check release publish build Develop > Continuous Integration
  7. build name: Build on: .. jobs: check: .. build: strategy:

    matrix: config: [ { target: android, os: ubuntu-latest, .. }, { target: apple, os: macos-latest, .. }, { target: windows, os: windows-latest, .. } { target: linux, os: ubuntu-latest, .. }, { target: js, os: ubuntu-latest, .. }, { target: desktop, os: ubuntu-latest, .. }, ] runs-on: ${{ matrix.config.os }} check release publish ▾ Matrix: build Build android Build apple Build desktop Build js Build linux Develop > Continuous Integration
  8. build name: Build on: .. jobs: check: .. build: strategy:

    matrix: config: [ { os: ubuntu-latest, tasks: testReleaseUnitTest .. }, { os: macos-latest, tasks: appleTest, .. }, { os: windows-latest, tasks: windowsTest, .. } { os: ubuntu-latest, tasks: linuxTest, .. }, { os: ubuntu-latest, tasks: jsTest .. }, { os: ubuntu-latest, tasks: desktopTest, .. }, ] runs-on: ${{ matrix.config.os }} check release publish ▾ Matrix: build Build android Build apple Build desktop Build js Build linux Develop > Test
  9. kotlinlang.org/docs/jvm-api-guidelines-backward-compatibility.html Develop > Compat . . “Backward-compatible code allows clients

    of newer API versions to use the same API code that they used with an older API version.”
  10. Develop > Compatibility build check ▾ Matrix: build Build android

    Build apple Build desktop Build js Build linux name: Build on: .. jobs: check: steps: - name: Check api run: ./gradlew apiCheck release publish
  11. name: Build jobs: publish: if: ${{ github.event_name != 'pull_request' }}

    steps: - name: Generate docs with dokka run: ./gradlew dokkaHtml - name: Upload artifact uses: actions/upload-pages-artifact@v1 with: path: ${{ github.workspace }}/kstore/build/dokka/html - name: Deploy to GitHub Pages id: deployment uses: actions/deploy-pages@v1 check release publish build Publish > Documentation ▾ Matrix: build
  12. Publish / Release repositories { mavenCentral() } /** Warning ⚠

    Incoming lots of configuration! * 1. Setup and link your OSSRH repository * 2. Include sources + documentation * 3. Sign your artefacts * */ https: // kotlinlang.org/docs/multiplatform-publish-lib.html
  13. publishing { repositories { maven { val isSnapshot = version.toString().endsWith("SNAPSHOT")

    val destination = if (!isSnapshot) "https: // s01.oss.sonatype.org/service/local/stagi else "https: // s01.oss.sonatype.org/content/repositories/snapshots" url = uri(destination) credentials { username = gradleLocalProperties(rootDir).getProperty("sonatypeUse password = gradleLocalProperties(rootDir).getProperty("sonatypePas } plugins { id("maven-publish") id("signing") } Publish / Release https: // central.sonatype.org/publish/publish-guide/ // 1. Link OSSRH repository
  14. credentials { username = gradleLocalProperties(rootDir).getProperty("sonatypeUse password = gradleLocalProperties(rootDir).getProperty("sonatypePas } }

    } publishing { val javadocJar = tasks.register<Jar>("javadocJar") { dependsOn(tasks.dokkaHtml) archiveClassifier.set("javadoc") from("$buildDir/dokka") } publications { withType<MavenPublication> { artifact(javadocJar) pom { name.set("KStore") description.set("A tiny Kotlin multiplatform library that assists licenses { Publish / Release https: // central.sonatype.org/publish/publish-guide/ // 2. Include sources + documentation
  15. release: if: ${{ github.event_name != 'pull_request' }} runs-on: macos-latest needs:

    - build steps: - name: Write secrets to local.properties if: ${{ github.event_name != 'pull_request' }} run: | echo sonatypeUsername="${SONATYPE_USERNAME}" >> "local. echo sonatypePassword="${SONATYPE_PASSWORD}" >> "local. echo gpgKeyPassword="${GPG_KEY_PASSWORD}" >> "local.pro echo gpgKeySecret="${GPG_KEY_SECRET}" >> "local.propert env: SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} GPG_KEY_PASSWORD: ${{ secrets.GPG_KEY_PASSWORD }} GPG_KEY_SECRET: ${{ secrets.GPG_KEY_SECRET }} - name: Release to sonatype run: ./gradlew publishAllPublicationsToMavenRepository check release publish build Publish / Release ▾ Matrix: build
  16. name: Build jobs: check: steps: - name: Generate kover coverage

    report run: ./gradlew koverXmlReport - name: Add coverage report to PR if: ${{ github.event_name == 'pull_request' }} id: kover uses: mi-kas/kover-report@v1 with: path: ${{ github.workspace }}/kstore/build/reports/kover/x token: ${{ secrets.GITHUB_TOKEN }} title: Code Coverage update-comment: true min-coverage-overall: 80 min-coverage-changed-files: 80 check release publish build Maintain > Code Coverage ▾ Matrix: build https: // github.com/mi-kas/kover-report