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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?