All writing

One Kotlin SDK for iOS and Android: API, packaging and versions

One Kotlin SDK for iOS and Android ships as a Maven artifact and an XCFramework through SwiftPM. Here is the API, versioning and licence work it needs.

One Kotlin codebase can be the SDK that both your iOS and Android integrators embed. Android teams get a Maven artifact, and iOS teams get an XCFramework through Swift Package Manager. Sharing the code is the easy part. An SDK holds up because of what sits around that code: an API that reads properly from Swift, version numbers integrators can trust, a licence list they can hand to their own lawyers, and documentation in each platform's language.

SDK and library development is part of the studio's Kotlin Multiplatform client work, and this post is the checklist that work follows. Every status and version below was checked against its linked source on 3 October 2026. For the wider question of whether KMP is ready at all, see Is Kotlin Multiplatform production-ready?.

What each integrator receives

Android. A Kotlin Multiplatform library publishes one Maven publication per target, plus a root publication called kotlinMultiplatform that, in JetBrains' words (page modified 13 May 2026), "automatically resolves to the appropriate platform-specific artifacts". One thing to know early: "By default, no artifacts of an Android library are published." You opt in through the androidLibrary {} block of the Android Gradle Library Plugin.

iOS. An XCFramework, zipped, with a Package.swift that declares it as a binaryTarget with a url and a checksum. The SwiftPM export guide (modified 21 July 2026) recommends keeping that manifest in a different repository from the Kotlin code: "Store the Package.swift file and the code that should be packaged into an XCFramework in separate Git repositories." Your iOS integrators add a small package that contains nothing but the manifest, and they never clone your Gradle build.

The Kotlin Gradle plugin registers assembleXCFramework and per-framework debug and release tasks once you declare an XCFramework() (native binaries docs, modified 13 May 2026). Any host can compile Kotlin for Apple targets, but you "still need to use a Mac machine" to build final binaries, and an XCFramework is a final binary. The release job therefore runs on macOS.

One framework per app

An XCFramework built from Kotlin "includes all of its dependencies", says the project configuration guide (modified 12 March 2026). If two Kotlin modules reach iOS as two separate frameworks, each one carries its own copy of what they share. The app gets bigger, and "any state passed by different modules through the same dependency won't be connected".

For an SDK that means two things. If you have split your SDK into several Kotlin modules, combine them into a single umbrella module, with its dependencies declared as api and export()ed, and ship that one XCFramework. And tell your integrators plainly that the SDK is a Kotlin/Native framework. An app that already ships a Kotlin framework of its own gets a second copy of every dependency the two share, and state does not connect between them.

Design the API for Swift as well as Kotlin

Start with explicit API mode, which JetBrains' library guidelines (modified 16 June 2026) recommend for library authors. Turn it on with explicitApi(). It makes every public declaration state its visibility and its type, so nothing becomes public API by accident.

Then remember what an iOS developer actually sees. A shipped SDK reaches Swift through the generated Objective-C header. Swift export is the replacement, but it is "currently in Alpha" (doc modified 28 August 2026), and it "currently works only in projects that use direct integration". An XCFramework delivered through SwiftPM is not direct integration, so Swift export is not an option for an SDK today. Design for the header. (The same boundary seen from inside one app, rather than an SDK, is covered in adding Kotlin Multiplatform to an existing Android app.)

The Objective-C interop docs (modified 12 August 2026) cover what that involves:

  • Names. Class names get a prefix "derived from the framework name", so choose the framework name as carefully as the classes. When two classes in different packages share a name, the compiler renames one, and "this algorithm is not stable yet and can change between Kotlin releases". Use @ObjCName to set the Swift name yourself, @HiddenFromObjC to keep Kotlin-only helpers out of the header, and @ShouldRefineInSwift where a thin Swift wrapper reads better than the generated signature.
  • Errors. Kotlin exceptions are unchecked and Swift errors are checked. A function marked @Throws hands its listed exceptions to Swift as NSError, and "Other Kotlin exceptions reaching Swift/Objective-C are considered unhandled and cause program termination." Every public function that can fail needs @Throws. Otherwise one of your bugs crashes your integrator's app.
  • Coroutines. suspend functions appear as completion handlers. Calling them as Swift async functions works, but the docs call it "highly experimental". The KMP FAQ (modified 1 October 2026) points to KMP-NativeCoroutines, "the more tried-and-tested solution", or SKIE, which "maps Kotlin Flow to Swift AsyncSequence directly".
  • Kotlin features Swift cannot see. JetBrains' Kotlin-Swift interopedia (last updated 3 November 2025) records that with default arguments "you always have to specify all the function arguments", and that a sealed class passed to a switch "requires a default case". Give Swift callers overloads or builders instead of relying on defaults.

One more number belongs in your integration guide: Kotlin 2.4.0 raised the default minimum for iOS from 14.0 to 15.0. Whatever deployment target your SDK compiles for becomes the floor for every app that embeds it.

Versioning and binary compatibility

JetBrains' backward-compatibility guide (modified 15 July 2026) separates binary compatibility, where a new version "can replace a previously compiled version" without recompiling, from source compatibility, where client code still compiles but must be rebuilt. An integrator who updates your SDK inside an app that also uses another library compiled against your old version needs the first kind.

Two of its rules catch most SDKs. "Avoid using data classes in your API": adding a property later changes the constructor, copy() and the componentN() functions. And adding a default argument breaks binary compatibility on the JVM, even though source still compiles. Kotlin 2.4.0 added an opt-in @IntroducedAt annotation that generates the older overloads for you. Writing the overloads by hand still works. When something has to go, follow the guide's deprecation cycle: @Deprecated at WARNING, then ERROR, then HIDDEN, and remove it only in a major release.

Do not rely on review to catch any of this. Since Kotlin 2.2.0 (23 June 2025, What's new), the Kotlin Gradle plugin has had its own binary compatibility validation, still Experimental and enabled with abiValidation() behind an opt-in. It covers "both JVM class files and klib formats". ./gradlew checkKotlinAbi compares the build with a committed ABI dump and runs as part of check. updateKotlinAbi rewrites the dump, so a pull request that changes the public API shows that change as a diff. The older standalone binary-compatibility-validator plugin (apiDump/apiCheck) is in maintenance mode. Its latest release, 0.18.2, came out on 2 September 2026, and new features go into the Gradle plugin instead.

Those dumps describe the Kotlin ABI. Your Swift integrators read the Objective-C header, and because the renaming algorithm can change between Kotlin releases, a compiler upgrade alone can change it. Diff that header between releases as well.

Publishing to Maven Central and as a Swift package

Sonatype's old OSSRH service reached end of life on 30 June 2025, so publishing now goes through the Central Publisher Portal. Its requirements include a -sources.jar and a -javadoc.jar beside every jar, a GPG .asc signature and .md5/.sha1 checksums for every file, and a POM with name, description, url, the licence, the developers and the SCM connection. JetBrains' Maven Central tutorial (modified 1 April 2026) does this with com.vanniktech.maven.publish 0.37.0 and its publishToMavenCentral or publishAndReleaseToMavenCentral tasks. Maven Central "explicitly forbids duplicate publications", so publish every target from one host, and that host is the Mac that builds the XCFramework.

Versions on Central are permanent: "we do not remove or modify components once they are publicly available" (immutability policy). A broken release is fixed with a new patch release, never by replacing the old one. If the SDK is private, Maven Central is the wrong home, because everything there is public. Use a private repository instead. The rest of this section still applies.

For iOS, upload the zip somewhere it can be downloaded directly and run swift package compute-checksum on it. If the hosted archive does not match the checksum in the manifest, Xcode shows an error. Apple recommends a semantic-version tag such as 1.2.4 on the package repository. Give the Maven artifact and the Swift package the same version number, so that one number means the same release on both platforms.

Licences, and the attribution file

Your SDK's licence goes in the POM, and Central requires it. The harder question is everything the SDK depends on. On iOS that code is compiled into your XCFramework, so the integrator's own tooling cannot see it, and your list is the only one they will get.

The studio's FAQ makes a specific promise here, and it describes the mechanism: "Every build emits an attribution file listing each dependency, version and licence, generated from the lockfile on CI so it cannot drift from what actually ships." Both parts are available in Gradle. Dependency locking writes the resolved versions to gradle.lockfile (./gradlew dependencies --write-locks), so the list describes a fixed graph. Cash App's Licensee plugin (1.14.1, 9 October 2025) works with the multiplatform plugin. It fails the build when a dependency's licence is not on your allowed list, and writes an artifacts.json with the licence of every artifact in the graph. For libraries, its README says to apply it to each library module. Generate the attribution file from that output and ship it in the release next to the zip and the AAR.

From the studio: an SDK the studio builds ships with that attribution file from every CI build, permissive dependencies by default, and anything copyleft raised for your decision before it goes in. If that is the SDK you need, describe it through the contact form.

Documentation for two kinds of integrator

Some documentation reaches iOS integrators for free. When the Objective-C header is generated, "KDoc comments from Kotlin code are translated into corresponding Objective-C comments", so they show up in Xcode. The same docs page lists the limits: comments from dependencies are not exported unless they were compiled with -Xexport-kdoc, and "many KDoc block tags, such as @property, are not supported". Write the KDoc on the declarations Swift actually sees.

For the reference site, Dokka renders HTML, Javadoc and Markdown from the same comments. Dokka 2.1.0 (15 October 2025) made the v2 Gradle plugin the default, and the latest stable release is 2.2.0, from 26 March 2026. Dokka describes the Kotlin API, though, and an iOS developer reading Kotlin signatures has to translate them in their head. The integration guide needs its own Swift examples, written against the real framework and compiled in CI, so they break when the header changes rather than when a customer copies them.

Two pages are worth writing on day one: a quick start per platform, which goes from adding the dependency to the first successful call, and a changelog per release that takes its API changes from the ABI dump diff and its header changes from the header diff.

Next step

If you are scoping an SDK for both platforms, start with three decisions: the framework name, the minimum iOS and Android versions, and whether the SDK is public. Most of what follows depends on those three. When you want help with the rest, the contact form reaches the person who would build it.

All writingNextWhy your prayer app and mosque disagree: Fajr, Isha and Asr by method