Kotlin MultiplatformでiOS/Android共通で使えるKotlinライブラリを作る
Kotlin Multiplatform (KMP) の可能性を探るため、共通のKotlinコードを、iOS向けのSwift Package・Android向けのMaven Artifactとして公開してみました。
本記事では、その具体的な手順と、KMPで共通ライブラリを作成することについての現時点での個人的な所感を記載します。
実際のソースコードは、以下のGitHubリポジトリにあります。
共通ライブラリ、サンプルのiOSアプリとAndroidアプリ
iOSアプリ向けのSwift Package
手順
以下の4つのステップで行いました。
- KMPLibrary (Kotlin製の共通ライブラリ) を作成する。
- Swift PackageをGitHubに公開する。
- Maven ArtifactをGitHub Packageに公開する。
- iOS/AndroidアプリでPackageを使用する。
以下の環境で実施しました。
- Xcode Version 16.1
- Android Studio Ladybug Feature Drop | 2024.2.2 Patch 2
1. KMP Library (共通ライブラリ) を作成する
Android StudioのNew Projectから、「Kotlin Multiplatform Library」を選択します。
また、Swift Packageとしての公開にXCFrameworkを利用するため、iOS framework distributionは「XCFramework」を選択します。
Projectの種類の選択 | XCFrameworkの選択 |
---|---|
![]() |
![]() |
今回の題材としては、Kotlinで書かれた優先度付きキュー (PriorityQueue) をiOS/Androidアプリでシェアすることとしました (実際のソースコード)。Kotlinコードは、shared/src/commonMain/kotlin/dev.kaseken.kmpsamplelibrary下に配置しました。
KMPSampleLibrary/
├── shared/
│ ├── src/
│ │ ├── commonMain/
│ │ │ ├── kotlin/
│ │ │ │ ├── dev/kaseken/kmpsamplelibrary/PriorityQueue.kt
│ │ ├── iosMain/ # (今回は空)
│ │ ├── androidMain/ # (今回は空)
│ │ ├── commonTest/ # テストコード
今回はPlatformに固有の処理を含めていないため、iOSMainやandroidMainには特に変更を加えていません。ビルドに成功すること、またcommonTest下に書いたテストコードがpassすることを確認しました。
※当初はcomparatorを渡すのではなくPriorityQueue<T: Comparable<T>>
のようなインタフェースを持つ実装にしていましたが、XCFramework化するとGenericsの型情報がObjective-Cヘッダに変換される際に T: AnyObject に変わってしまう (Comparableの情報が消えてしまう) ため、Swift側で型安全に扱えなくなるという問題がありました。この問題を回避するため、Comparatorを渡す設計に変更しました。Genericsを使ったコードのSwiftとの相互運用性が低いというのは、KMPの難点の一つかと思いました。
2. Swift PackageをGitHubに公開する
以下の3つのステップで行いました。
2-1. Gradleタスクで、KMP LibraryからXCFrameworkを生成する。
2-2. Swift Packageを作成し、XCFrameworkのWrapperコードを書く。
2-3. Swift PackageをGitHub上にpushする。
2-1. XCFrameworkの生成
build.gradle.kts
のkotlin
ブロック内に、以下のようなコードを追加します。
"KMPSampleLibrary"に名前を変えているところ以外は、Project作成時にXCFrameworkを選択していれば、デフォルトで存在するかと思います。
val xcf = XCFramework("KMPSampleLibrary")
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
it.binaries.framework {
baseName = "KMPSampleLibrary"
xcf.add(this)
}
}
上記のように設定すると、assembleKMPSampleLibraryXCFramework
のようなGradleタスクが使用可能になります。ただし、KMPSampleLibraryの部分は各々の設定した名前に依存するため、./gradlew tasks
で実際のタスク名をご確認ください。
./gradlew assembleKMPSampleLibraryXCFramework
を実行すると、Debug/Relase両方のXCFrameworkがbuildディレクトリ内に生成されます。
% ./gradlew assembleKMPSampleLibraryXCFramework
> Task :shared:assembleKMPSampleLibraryDebugXCFramework
xcframework successfully written out to: /Users/kentkaseda/Projects/KMPSample/KMPSampleLibrary/shared/build/XCFrameworks/debug/KMPSampleLibrary.xcframework
> Task :shared:assembleKMPSampleLibraryReleaseXCFramework
xcframework successfully written out to: /Users/kentkaseda/Projects/KMPSample/KMPSampleLibrary/shared/build/XCFrameworks/release/KMPSampleLibrary.xcframework
上記のKMP LibraryからXCFrameworkを作成する方法に関しては、以下の公式ドキュメント・ブログ記事が参考になりました。
- Build XCFrameworks:
https://kotlinlang.org/docs/multiplatform-build-native-binaries.html#build-xcframeworks - 【KMM】sharedモジュールをXCFrameworkとして生成する:
https://qiita.com/yamakentoc/items/d5f96374b3e638602115
2-2. Swift Packageを作成し、XCFrameworkのWrapperコードを書く
Xcodeを開き、File > New > Packageから新規Swift Packageを作成します。
2-1で作成したXCFramework (Releaseビルド) を、Swift Package内にコピーし、package.swift
上でbinaryTargetとしてtargetに追加します。これにより、このSwift Package内でimport KMPSampleLibrary
のようにして、先ほど作成したPriorityQueueを使用可能となります。
// swift-tools-version: 6.0
// The swift-tools-version declares the minimum version of Swift required to build this package.
import PackageDescription
let package = Package(
name: "KMPSampleSwiftPackage",
products: [
.library(
name: "KMPSampleSwiftPackage",
targets: ["KMPSampleSwiftPackage"]
),
],
targets: [
.binaryTarget(
name: "KMPSampleLibrary",
path: "KMPSampleLibrary.xcframework"
),
.target(
name: "KMPSampleSwiftPackage",
dependencies: [
"KMPSampleLibrary",
]
),
.testTarget(
name: "KMPSampleSwiftPackageTests",
dependencies: [
"KMPSampleSwiftPackage",
]
),
]
)
Swift Package内に、KMPライブラリのWrapperコード (実際のソースコード) を追加しました。
なお、iOS向けにKMPライブラリをビルドすると、Kotlin/NativeのObjective-Cヘッダー(.hファイル)が自動生成されます。今回のPriorityQueueの場合、以下のようなコードが生成されます。
__attribute__((objc_subclassing_restricted))
__attribute__((swift_name("PriorityQueue")))
@interface KMPSLPriorityQueue<T> : KMPSLBase
- (instancetype)initWithComparator:(KMPSLInt *(^)(T _Nullable, T _Nullable))comparator __attribute__((swift_name("init(comparator:)"))) __attribute__((objc_designated_initializer));
- (BOOL)isEmpty __attribute__((swift_name("isEmpty()")));
- (T _Nullable)peek __attribute__((swift_name("peek()")));
- (T _Nullable)pop __attribute__((swift_name("pop()")));
- (void)pushElement:(T _Nullable)element __attribute__((swift_name("push(element:)")));
- (int32_t)size __attribute__((swift_name("size()")));
@end
Kotlin側ではcomparatorが(T,T)->Int
であったのに対し、Objective-Cのインターフェースでは(T?,T?)->Int
に変わっています。
また、push(element:T)
もpush(element:T?)
に変換されています。これはKotlinのnull安全性とObjective-C/Swiftのnullabilityの違いによるもので、Swiftとの相互運用性に難がある部分の一つです。
2-3. Swift PackageをGitHub上にpushする
2-2で作成したSwift PackageをGitHub上にpushするだけで、Swift Packageとして公開されます。以下が実際のリポジトリです。
3. Maven ArtifactをGitHub Packagesに公開する
Android向けのPackageの公開には、Github PackagesのGradle registryを利用しました。
Maven centralに登録する等も考えられますが、アカウント登録等が必要なため、より簡単に登録できるGitHub Packagesを選択しました。
以下の手順のように、build.gradle.kts
にPublish用の設定を行い、また認証用にPersonal Access Token (PAT) を作成します。PATにはread:packagesスコープが必要です。
最終的に、共通ライブラリのbuild.gradle.kts
は以下のような状態になりました (実際のソースコード)。
./gradlew publish
を実行すると、GitHubリポジトリ上にMaven Artifactが公開されます。
例) https://github.com/kaseken?tab=packages&repo_name=KMPSampleLibrary
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.plugin.mpp.apple.XCFramework
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
id("maven-publish")
}
kotlin {
androidTarget {
compilations.all {
compileTaskProvider.configure { compilerOptions { jvmTarget.set(JvmTarget.JVM_1_8) } }
}
}
val xcf = XCFramework("KMPSampleLibrary")
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
it.binaries.framework {
baseName = "KMPSampleLibrary"
xcf.add(this)
}
}
sourceSets {
commonMain.dependencies {
// put your multiplatform dependencies here
}
commonTest.dependencies { implementation(libs.kotlin.test) }
}
}
android {
namespace = "dev.kaseken.kmpsamplelibrary"
compileSdk = 35
defaultConfig { minSdk = 29 }
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}
group = "com.github.kaseken"
version = "1.0.8"
publishing {
repositories {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/kaseken/KMPSampleLibrary")
credentials {
// 環境変数にGitHubのユーザー名とPATのトークンを入れる。
username = System.getenv("GITHUB_USERNAME") ?: ""
password = System.getenv("GITHUB_TOKEN") ?: ""
}
}
}
publications {
register<MavenPublication>("gpr") {
from(components["kotlin"])
groupId = project.group.toString()
artifactId = "kmpsamplelibrary"
version = project.version.toString()
}
}
}
4. iOS/Androidアプリで公開したPackageを利用する
iOSアプリでSwift Packageを利用する
利用したいiOSアプリで、XcodeのFile > Add Package Dependenciesから、2-3で作成したGitHubのURLで検索し、Swift Packageを追加します。
Ref) https://developer.apple.com/documentation/xcode/adding-package-dependencies-to-your-app#Add-a-package-dependency
AndroidアプリでMaven Artifactを利用する
利用したいAndroidアプリで、3.で作成したMaven Artifactをbuild.gradle.kts
でimplementation("com.github.kaseken:kmpsamplelibrary:1.0.0")
のようにdependencyに追加します (実際のソースコード)。
また、settings.gradle.kts
にGitHub Packageのurlとcredentialを追加する必要があります (実際のソースコード)。
Maven Artifactをpublicに公開している場合であっても、credentialの追加は必要でした。
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
// Configurations for searching GitHub Packages.
maven {
url = uri("https://maven.pkg.github.com/kaseken/KMPSampleLibrary")
credentials {
username = System.getenv("GITHUB_USERNAME") ?: throw Exception("GITHUB_USERNAME is not set")
password = System.getenv("GITHUB_TOKEN") ?: throw Exception("GITHUB_TOKEN is not set")
}
}
}
}
今回共通ライブラリ化したPriorityQueueを利用して、配列から最小の要素を取り出す処理を両アプリに追加し、問題なく動いていることを確認しました。
iOS | Android |
---|---|
![]() |
![]() |
KMP Libraryに対する所感
KMPを利用してiOS/Androidアプリ向けの共通ライブラリを作成することについて、現時点での所感を書きます。
共通のPackageを作ること自体は難しくないと感じました。
一方で、Kotlinでコードを共通化できることのメリット以上に、Swift PackageからKMPコードを参照する際のSwiftとの相互運用性の低さのデメリットが気になりました。先述した通り、Genericsの型情報やNull安全性がObjective-Cの制限により失われる点など、少し試した限りでも使いにくさを感じる部分がいくつかありました。
以下のブログ記事でもObjective-Cのheaderファイルを介することに由来する制限が短所として挙げられていますが、その通りだと思います。
個人的な結論としては、インタフェースが単純な一方で、内部実装が複雑な処理などをKotlinコードで共通化するなどは考えられるものの、より一般的に積極的にKMPでコードを共通化していくのは難しさがあると感じました。今後、Swiftとの相互運用性が改善すれば、より実用可能性が高まると考えます。
Discussion