- Kotlin
Kotlin Multiplatform入門|共通コードとexpect/actual
Kotlin Multiplatform(KMP)は、1つの Kotlin のコードを Android・iOS・デスクトップ(JVM)・Web などに向けてコンパイルするしくみです。 全プラットフォーム共通のコードを commonMain に書き、プラットフォームごとに違う部分だけを expect(共通側の宣言)と actual(各プラットフォームの実装)で書き分けます。
// commonMain:宣言だけ
expect fun platformName(): String
class Greeting {
fun greet() = "Hello from ${platformName()}"
}
// jvmMain:JVM の実装
actual fun platformName(): String = "JVM ${System.getProperty("java.version")}"
// jsMain:JS の実装
actual fun platformName(): String = "JS"
同じ commonMain を、kotlinc で JVM 向けと JS 向けにコンパイルして実行した結果です。
$ kotlin -cp outC sample.Platform_jvmKt # JVM
Hello from JVM 21.0.10 at true | sep=1
$ node outJs/app.js # JS(Node.js)
Hello from JS at true | sep=1
| ソースセット | 中に書くもの | 使える API |
|---|---|---|
commonMain |
ロジック・データ・通信・expect の宣言 |
Kotlin 標準ライブラリとマルチプラットフォーム対応ライブラリだけ |
androidMain |
Android の actual |
Android SDK・Java |
iosMain |
iOS の actual(iPhone 実機とシミュレーターで共通) |
Foundation・UIKit などの Apple の API |
jvmMain |
デスクトップ・サーバーの actual |
Java(JDK)のすべて |
commonTest |
共通のテスト | kotlin.test |
確認環境:Kotlin コンパイラ kotlinc 2.4.20、OpenJDK 21.0.10、Node.js 22.22.2(Linux)。この記事の expect/actual のコードを、kotlinc-jvm と kotlinc-js でコンパイル・実行し、間違えたときのエラー文を記録しました。Gradle のプロジェクト構成、ソースセット、Mac が要る場面、Compose Multiplatform の安定度は、kotlinlang.org・developer.android.com・ktor.io のドキュメントで確かめました。ライブラリの版は各 GitHub リポジトリのタグで確かめました(確認日 2026年10月13日)。
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin Multiplatform入門|共通コードとexpect/actual」
KMP で何を共通にできるか
KMP は「UI まで全部共通」にも「ロジックだけ共通」にもできます。
| 共通にする範囲 | UI | 使うもの |
|---|---|---|
| ロジックだけ | 各プラットフォームの UI(Android は Jetpack Compose、iOS は SwiftUI) | KMP |
| ロジック+UI | 共通の UI | KMP+Compose Multiplatform |
Compose Multiplatform は、KMP の上で UI まで共通にする JetBrains のフレームワークです。 Android の Jetpack Compose と同じ書き方で、iOS・デスクトップ・Web の画面を作ります。KMP だけを使って、UI は各プラットフォームのものにする構成もよく使われます。既存の Android アプリと iOS アプリがあり、通信やデータの処理だけをそろえたい場合などです。
kotlinlang.org の「Stability of supported platforms」による安定度です。
| プラットフォーム | KMP(共通コード) | Compose Multiplatform(UI) |
|---|---|---|
| Android | Stable | Stable |
| iOS | Stable | Stable |
| デスクトップ(JVM) | Stable | Stable |
| サーバー(JVM) | Stable | ― |
| Web(Kotlin/JS) | Stable | 記載なし |
| Web(Kotlin/Wasm) | Beta | Beta |
Jetpack Compose の書き方は、関連記事「Jetpack Compose入門|基本の部品と状態管理の早見表」で扱っています。
プロジェクト構成
IntelliJ IDEA か Android Studio の New Project で「Kotlin Multiplatform」を選ぶと、ひな形ができます(kotlinlang.org の Quickstart による)。共通コードのモジュールの中は、次のようなソースセット(ソースのまとまり)に分かれます。
shared/
└── src/
├── commonMain/kotlin/ ← 全プラットフォーム共通
├── commonTest/kotlin/ ← 共通のテスト
├── androidMain/kotlin/ ← Android だけ
├── iosMain/kotlin/ ← iOS だけ(iosArm64・iosSimulatorArm64 で共通)
└── jvmMain/kotlin/ ← デスクトップ・サーバーだけ
ソースセットのルール
kotlinlang.org の「The basics of Kotlin Multiplatform project structure」の要点です。
commonMainのコードは、宣言したすべてのターゲットへコンパイルされます。 そのため、プラットフォーム固有の API は使えません。ドキュメントでは、java.io.Fileはネイティブ(iOS など)に無いのでエラーになる、と例が挙がっています。jvmMainからはcommonMainを使えますが、逆はできません。- JVM 向けにコンパイルするときは、
commonMainとjvmMainがまとめてコンパイルされます。 iosMainのような中間のソースセットは、一部のターゲットだけで共有されます。iosArm64(実機)とiosSimulatorArm64(シミュレーター)を宣言すると、Kotlin の Gradle プラグインが「デフォルトの階層テンプレート」からiosMain・appleMain・nativeMainなどを自動で作ります。iosMainではplatform.Foundation.NSUUIDのような Apple の API を使えます。- テストは
commonTest・jvmTestのように、各ソースセットに対応するTestが付いたものに書きます。テストのライブラリはkotlin.testです。
build.gradle.kts の形
ドキュメントの DSL リファレンスと Android の KMP プラグインのページをもとにした、共通モジュールの設定です。
// shared/build.gradle.kts
plugins {
kotlin("multiplatform") version "2.4.21"
kotlin("plugin.serialization") version "2.4.21"
id("com.android.kotlin.multiplatform.library") // Android のライブラリ用
}
kotlin {
android {
namespace = "com.example.shared"
compileSdk = 36
minSdk = 24
}
iosArm64()
iosSimulatorArm64()
jvm()
sourceSets {
commonMain.dependencies {
implementation("io.ktor:ktor-client-core:3.6.0")
implementation("io.ktor:ktor-client-content-negotiation:3.6.0")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.6.0")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0")
}
androidMain.dependencies {
implementation("io.ktor:ktor-client-okhttp:3.6.0")
}
iosMain.dependencies {
implementation("io.ktor:ktor-client-darwin:3.6.0")
}
jvmMain.dependencies {
implementation("io.ktor:ktor-client-okhttp:3.6.0")
}
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
- プラグインの版は Kotlin の版と同じです(DSL リファレンスの例は
2.4.21)。 - Android のターゲットは、
kotlin { }の中のandroid { }で書きます。 developer.android.com によると、com.android.kotlin.multiplatform.libraryプラグインは AGP 8.10.0 以上・KGP 2.0.0 以上が前提で、AGP 8.12.0 以上ではandroid { }、それ未満ではandroidLibrary { }を使います。以前のandroidTarget()とcom.android.libraryの組み合わせについては、KMP 向けの API が AGP 9.0 以上で非推奨・オプトイン扱いになり、AGP 10.0 で削除予定とされています。 - 依存は
commonMain.dependencies { }のように、ソースセットごとに書きます。
Gradle の Kotlin DSL の書き方は、関連記事「build.gradle.kts書き方|Groovy対応表とバージョンカタログ」で扱っています。
expect / actual の書き方
expect は「この名前のものを、各プラットフォームで必ず用意する」という共通側の約束です。actual が各プラットフォームの中身です。kotlinlang.org の「Expected and actual declarations」によるルールです。
| ルール | 内容 |
|---|---|
| どこに書くか | expect は commonMain、actual は各プラットフォームのソースセット |
| 名前とパッケージ | expect と同じ名前・同じパッケージにする |
| すべてのターゲットに | 宣言したターゲットの数だけ actual が要る(iosMain で iOS 全部をまとめられる) |
expect に中身 |
書けない(本体の無い宣言だけ) |
| 使えるもの | 関数・プロパティ・クラス・インターフェース・enum・アノテーション・object |
クラスの expect/actual |
Beta。警告を消すには -Xexpect-actual-classes |
完成コード
関数・プロパティ・クラスの3種類を expect にした例です。この記事の最初の実行結果は、このコードです。
// commonMain/Platform.kt
package sample
/** 各プラットフォームで中身を用意する宣言 */
expect fun platformName(): String
expect val lineSeparator: String
expect class Clock() {
fun nowMillis(): Long
}
/** 共通コード:どのプラットフォームでも同じ */
class Greeting(private val clock: Clock = Clock()) {
fun greet(): String = "Hello from ${platformName()} at ${clock.nowMillis() > 0}"
}
fun report(): String = listOf(Greeting().greet(), "sep=${lineSeparator.length}").joinToString(" | ")
// jvmMain/Platform.jvm.kt
package sample
actual fun platformName(): String = "JVM ${System.getProperty("java.version")}"
actual val lineSeparator: String = System.lineSeparator()
actual class Clock actual constructor() {
actual fun nowMillis(): Long = System.currentTimeMillis()
}
fun main() = println(report())
// jsMain/Platform.js.kt
package sample
actual fun platformName(): String = "JS"
actual val lineSeparator: String = "\n"
actual class Clock actual constructor() {
actual fun nowMillis(): Long = kotlin.js.Date.now().toLong()
}
fun main() = println(report())
iOS の actual は、iosMain に次のように書きます(kotlinlang.org の例にならった形です)。
// iosMain/Platform.ios.kt
package sample
import platform.Foundation.NSDate
import platform.Foundation.timeIntervalSince1970
import platform.UIKit.UIDevice
actual fun platformName(): String =
UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion
actual val lineSeparator: String = "\n"
actual class Clock actual constructor() {
actual fun nowMillis(): Long = (NSDate().timeIntervalSince1970 * 1000).toLong()
}
kotlinc で expect/actual をコンパイルする
ふだんは Gradle がすべて行いますが、しくみを確かめるために kotlinc で直接コンパイルしました。2つのファイルをそのまま渡すだけでは、コンパイルできませんでした。
$ kotlinc-jvm commonMain/Platform.kt jvmMain/Platform.jvm.kt -d outA
commonMain/Platform.kt:4:1: error: 'expect' and 'actual' declarations can be used only in multiplatform projects. Learn more about Kotlin Multiplatform: https://kotl.in/multiplatform-setup
commonMain/Platform.kt:4:12: error: platformName: 'expect' and corresponding 'actual' are declared in the same module.
$ kotlinc-jvm -Xmulti-platform commonMain/Platform.kt jvmMain/Platform.jvm.kt -d outB
commonMain/Platform.kt:4:12: error: platformName: 'expect' and corresponding 'actual' are declared in the same module.
commonMain/Platform.kt:14:41: error: overload resolution ambiguity between candidates:
-Xmulti-platform だけでは、expect と actual が同じ1つのまとまり(モジュール)にあると見なされます。「common」と「jvm」の2つの断片(fragment)に分け、jvm が common を引き継ぐ(refines)と指定すると、通りました。
$ kotlinc-jvm -Xmulti-platform \
-Xfragments=common,jvm \
-Xfragment-sources=common:commonMain/Platform.kt,jvm:jvmMain/Platform.jvm.kt \
-Xfragment-refines=jvm:common \
commonMain/Platform.kt jvmMain/Platform.jvm.kt -d outC
commonMain/Platform.kt:8:1: warning: 'expect'/'actual' classes (including interfaces, objects, annotations, enums, and 'actual' typealiases) are in Beta. Consider using the '-Xexpect-actual-classes' flag to suppress this warning. Also see: https://youtrack.jetbrains.com/issue/KT-61573
$ kotlin -cp outC sample.Platform_jvmKt
Hello from JVM 21.0.10 at true | sep=1
outC/sample には Clock.class・Greeting.class・PlatformKt.class・Platform_jvmKt.class ができました。expect class には Beta の警告が出ます。-Xexpect-actual-classes を足すと警告は消えました。
JS は、kotlinc-js で同じ指定(-Xfragments=common,js など)をして klib(Kotlin のライブラリ形式)を作り、それを JS に変換しました。出力の app.js は 607,721 バイトで、Node.js で上の結果になりました。Gradle の KMP プラグインは、この「ソースセットごとの断片とそのつながり」を自動で組み立てています。
expect/actual のエラー文(実際に出た原文)
わざと間違えてコンパイルした結果です。
| 間違い | エラー文 |
|---|---|
actual を用意し忘れた |
the 'expect' declaration 'lineSeparator' has no 'actual' declaration in module '<common> for JVM'. |
| 戻り値の型が違う | the 'expect' and the 'actual' declarations are incompatible. / reason: the return types are different |
expect に本体を書いた |
'expect' declaration cannot have a body. |
actual のパッケージが違う |
'public final actual fun platformName(): String' has no corresponding 'expect' declaration |
actual を付け忘れた |
declaration must be marked with 'actual'. |
commonMain で JVM の API を使った(JS 向け) |
unresolved reference 'System'. / unresolved reference 'java'. |
commonMain で System.currentTimeMillis() や java.io.File を使ったコードは、kotlinc-jvm で JVM とだけ組んだときはエラーになりませんでした。 JVM 向けのコンパイルでは JDK が見えるためです。JS 向けにコンパイルすると、上の unresolved reference になりました。Gradle の KMP では、commonMain は宣言したすべてのターゲットに向けて扱われるので、ドキュメントのとおり共通コードでのプラットフォーム API の使用はエラーになります。共通コードの誤りは、JVM 以外のターゲットを1つ足しておくと見つかりやすくなります。
actual typealias:既存のクラスをそのまま使う
プラットフォームに既にあるクラスを actual にしたいときは、actual typealias を使います。
// commonMain
expect class AtomicCounter(initial: Int) {
fun incrementAndGet(): Int
fun get(): Int
}
fun countTwice(): Int { val c = AtomicCounter(0); c.incrementAndGet(); return c.incrementAndGet() }
// jvmMain
actual typealias AtomicCounter = java.util.concurrent.atomic.AtomicInteger
kotlinc-jvm でコンパイルして実行すると、countTwice=2 と出ました。AtomicInteger のコンストラクターとメソッドが、expect の宣言とそのまま対応しています。
expect/actual より interface が向いている場面
kotlinlang.org は、できるところでは、ふつうの interface を使うことを勧めています。
| 方法 | 向いている場面 |
|---|---|
expect fun・expect val |
1つの値や関数を、プラットフォームごとに1つ用意する |
interface+expect fun のファクトリー |
実装を差し替えたい(テスト用の偽物を入れたい) |
expect class |
プラットフォームの型を継承する必要がある(Beta) |
actual typealias |
プラットフォームに同じ形のクラスが既にある |
interface にすると、1つのプラットフォームに実装を複数持てて、テストで偽物に差し替えやすくなります。interface の書き方は、関連記事「Kotlin interfaceの使い方 abstract classとの違い」で扱っています。
// commonMain
interface Identity { val userName: String }
expect fun buildIdentity(): Identity
// jvmMain
actual fun buildIdentity(): Identity = JvmIdentity()
class JvmIdentity(override val userName: String = System.getProperty("user.name") ?: "none") : Identity
ライブラリの選び方
commonMain には、マルチプラットフォーム対応のライブラリしか入れられません。 Android や JVM でよく使うライブラリの多くは JVM 専用なので、共通コードでは代わりのものを使います。版は2026年10月13日時点の各 GitHub リポジトリのタグです。
| 用途 | JVM・Android でよく使うもの | 共通コード(KMP)で使うもの | 版 |
|---|---|---|---|
| HTTP 通信 | Retrofit・OkHttp | Ktor client | 3.6.0 |
| JSON | Gson・Jackson・Moshi | kotlinx.serialization | 1.11.0 |
| 非同期 | ― | kotlinx.coroutines | 1.11.0 |
| 日付・時刻 | java.time | kotlinx-datetime | 0.8.0 |
| データベース | Room・JDBC | SQLDelight | 2.4.1 |
| UI | Jetpack Compose・SwiftUI | Compose Multiplatform | 1.12.1 |
選ぶときの目安です。
- ライブラリの README やドキュメントで、対応するターゲット(Android・iOS・JVM・JS・Wasm)を確かめます。
- Ktor client は、通信の部分(エンジン)がプラットフォームごとに分かれています。ktor.io によると、OkHttp は JVM と Android、Darwin は iOS・macOS などの Apple、Js は JavaScript、CIO は JVM・Android・Native・JS・WasmJs に対応します。
commonMainにktor-client-engine-defaultsを入れると、ターゲットごとの既定のエンジンが使われます。上の build.gradle.kts のように、各ソースセットにエンジンを入れる形もあります。 - 無いものは、
expect/actualか interface で自分で用意します。
共通コードの例:Ktor client+kotlinx.serialization
commonMain に書く通信のコードです。Android でも iOS でも同じコードが動き、エンジンだけがプラットフォームごとに変わります。
// commonMain/TodoApi.kt
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@Serializable
data class Todo(val id: Int, val title: String, val completed: Boolean)
class TodoApi(
private val client: HttpClient = HttpClient {
install(ContentNegotiation) {
json(Json { ignoreUnknownKeys = true })
}
},
) {
suspend fun fetch(id: Int): Todo =
client.get("https://jsonplaceholder.typicode.com/todos/$id").body()
}
kotlinx.serialization の設定は、関連記事「kotlinx.serialization使い方|JSON変換・設定一覧・エラー」で扱っています。suspend 関数とコルーチンは、関連記事「Kotlinコルーチン入門|launch・async・runBlocking」で扱っています。
ビルドの前提:Mac が要る場面
| やりたいこと | Mac が要るか | 根拠 |
|---|---|---|
| iOS アプリをビルド・実行する | 要る(Xcode も) | kotlinlang.org の Quickstart |
| iOS 向けの最終的なバイナリ(framework など)を作る | 要る | Kotlin/Native のターゲット対応表 |
| cinterop(CocoaPods を含む)を使う iOS のビルド | 要る | 同上 |
| iOS 向けの klib(ライブラリ)だけを作る(cinterop なし) | Linux・Windows でも可 | 同上 |
| Android・デスクトップ(JVM)・Web のビルド | 要らない | ― |
| 共通コードのテスト(JVM で実行) | 要らない | ― |
Kotlin/Native のターゲット対応表では、iosArm64・iosSimulatorArm64・iosX64 は「Apple macOS hosts only」とされています。iosArm64・iosSimulatorArm64 は Tier 1(CI で常にテストされる)、iosX64(Intel Mac のシミュレーター)は Tier 3 です。Linux と Windows では「Apple targets 以外」の最終バイナリを作れます。
Windows・Linux の開発者がいるチームでは、共通コードと Android の開発は各自の PC で行い、iOS のビルドと確認は Mac(または macOS の CI)で行う分担になります。 Xcode は一度起動して初期設定を済ませておき、更新のたびにも起動するよう、Quickstart に書かれています。
よくある質問
Q. KMP と Flutter・React Native の違いは何ですか。 A. KMP は Kotlin のコードを各プラットフォームの形式(Android は JVM のバイトコード、iOS はネイティブのバイナリ)にコンパイルします。UI を共通にするかどうかは選べます。既存のネイティブアプリに、共通のロジックだけを少しずつ足す使い方ができる点が特徴です。
Q. iOS 側からは、共通コードをどう呼びますか。
A. 共通モジュールは iOS 向けに framework としてビルドされ、Swift から import shared のように読み込んで使います。このビルドは Mac の Xcode で行います。
Q. androidMain と jvmMain の両方が要りますか。
A. Android とデスクトップ(またはサーバー)の両方を対象にするときは両方を宣言します。中身が同じになる部分は、ソースセットを自分で追加して共有する方法も、ドキュメントの「Hierarchical project structure」で説明されています。
Q. Kotlin の文法から確認したいです。 A. 関連記事「Kotlin入門|Javaとの違いと基本文法をコピペで学ぶ【2.4対応】」で扱っています。
まとめ
- KMP は1つの Kotlin のコードを各プラットフォームへコンパイルするしくみ。共通コードは
commonMain、違う部分は各ソースセットに書く expectは共通側の宣言、actualは各プラットフォームの中身。同じ名前・同じパッケージで、すべてのターゲットに要る- kotlinc では、
-Xmulti-platformと-Xfragments・-Xfragment-refinesで断片を分けると、同じ共通コードが JVM と JS で動いた commonMainで JVM の API を使うと、JS 向けでunresolved referenceになった。JVM とだけ組むと見逃す- 差し替えたい実装は、
expectより interface が扱いやすい。expect classは Beta - ライブラリは Ktor client・kotlinx.serialization・kotlinx-datetime・SQLDelight など、マルチプラットフォーム対応のものを使う
- UI まで共通にするなら Compose Multiplatform(iOS も Stable)
- iOS のビルドには Mac と Xcode が要る。Android・デスクトップ・Web と共通コードのテストは Mac なしでできる
参考・出典
確認日はいずれも 2026年10月13日です。
- Kotlin「The basics of Kotlin Multiplatform project structure」:https://kotlinlang.org/docs/multiplatform/multiplatform-discover-project.html
- Kotlin「Expected and actual declarations」:https://kotlinlang.org/docs/multiplatform/multiplatform-expect-actual.html
- Kotlin「Hierarchical project structure」:https://kotlinlang.org/docs/multiplatform/multiplatform-hierarchy.html
- Kotlin「Multiplatform Gradle DSL reference」:https://kotlinlang.org/docs/multiplatform/multiplatform-dsl-reference.html
- Kotlin「Quickstart」:https://kotlinlang.org/docs/multiplatform/quickstart.html
- Kotlin「Stability of supported platforms」:https://kotlinlang.org/docs/multiplatform/supported-platforms.html
- Kotlin「Kotlin/Native target support」:https://kotlinlang.org/docs/native-target-support.html
- Android Developers「Set up the Android Gradle Library Plugin for KMP」:https://developer.android.com/kotlin/multiplatform/plugin
- Ktor「Client engines」:https://ktor.io/docs/client-engines.html
- JetBrains/compose-multiplatform:https://github.com/JetBrains/compose-multiplatform
- ktorio/ktor:https://github.com/ktorio/ktor
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「Kotlin Multiplatform入門|共通コードとexpect/actual」
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。