Kotlin: Kotlinマルチプラットフォーム入門
最終更新:2026-08-26
KMPはKotlinの究極のビジョンです。CharlieのOrderValidatorは一度記述するだけで、JVMバックエンド、iOSアプリ、JSフロントエンド、Wasmブラウザで再利用されます。expect/actualの仕組みにより、プラットフォーム間の違いは境界部分に限られます。
1. 学習内容
- KMPアーキテクチャ:
commonMain/jvmMain/iosMain/jsMain - モジュール共有の戦略:ネットワーク、データモデル、ビジネスロジック
- Gradleのマルチプラットフォーム設定
- 相互運用性:JVM ↔ Java / iOS ↔ Swift / JS ↔ JavaScript
- チャリーの活躍:
OrderValidatorがcommonMainで共有しました
2. 本物の建築家の物語
(1) 課題:3つのプラットフォームにまたがってビジネスロジックが重複している
Charlieの会社では、JVMバックエンド、iOSアプリ、JSフロントエンドを採用しています。同じOrderValidatorロジックが、Java、Swift、TypeScriptの3つの言語でそれぞれ実装されていました。1つのバグを修正するには3か所を変更する必要があり、これが原因で1か月間に2件の動作不一致が発生しました。
(2) KMPの共有コードソリューション
KOTLIN
// commonMain: ONE implementation for all platforms
class OrderValidator {
fun validate(order: Order): List<String> {
val errors = mutableListOf<String>()
if (!order.id.startsWith("ORD-")) errors.add("Invalid ID")
if (order.total < 0) errors.add("Negative total")
return errors
}
}
1つのコードベースで3つのプラットフォームに対応。バグを1回修正するだけで、すべてのプラットフォームで動作が自然に一貫します。
3. KMPアーキテクチャ
(1) プロジェクトの構成
TEXT
📖 参照専用
shared/
├── src/
│ ├── commonMain/kotlin/ # Shared code (all platforms)
│ │ └── com/order/
│ │ ├── Order.kt
│ │ ├── OrderValidator.kt
│ │ └── Platform.kt # expect declarations
│ ├── commonTest/kotlin/ # Shared tests
│ ├── jvmMain/kotlin/ # JVM-specific code
│ │ └── com/order/
│ │ └── Platform.kt # actual for JVM
│ ├── iosMain/kotlin/ # iOS-specific code
│ │ └── com/order/
│ │ └── Platform.kt # actual for iOS
│ └── jsMain/kotlin/ # JS-specific code
│ └── com/order/
│ └── Platform.kt # actual for JS
└── build.gradle.kts
(2) 期待値/実績値のメカニズム
KOTLIN
// commonMain: declare what you need (expect)
expect fun getPlatformName(): String
expect class DateFormatter() {
fun format(timestamp: Long): String
}
// jvmMain: provide JVM implementation (actual)
actual fun getPlatformName(): String = "JVM"
actual class DateFormatter actual constructor() {
actual fun format(timestamp: Long): String =
java.text.SimpleDateFormat("yyyy-MM-dd").format(timestamp)
}
// iosMain: provide iOS implementation (actual)
actual fun getPlatformName(): String = "iOS"
actual class DateFormatter actual constructor() {
actual fun format(timestamp: Long): String {
// Use NSDateFormatter
return NSDateFormatter().apply {
dateFormat = "yyyy-MM-dd"
}.stringFromDate(NSDate(timestamp / 1000.0))
}
}
// jsMain: provide JS implementation (actual)
actual fun getPlatformName(): String = "JS"
actual class DateFormatter actual constructor() {
actual fun format(timestamp: Long): String {
// Use JavaScript Date
return js("new Date(timestamp).toISOString().split('T')[0]")
}
}
(3) KMPマルチプラットフォームアーキテクチャ
flowchart TD
A[commonMain<br/>Shared Business Logic] --> B[jvmMain<br/>JVM Actual]
A --> C[iosMain<br/>iOS Actual]
A --> D[jsMain<br/>JS Actual]
A --> E[wasmMain<br/>Wasm Actual]
B --> B1[Spring Boot<br/>Backend]
C --> C1[iOS App<br/>Swift Interop]
D --> D1[Node.js / Browser]
E --> E1[Web Assembly Runtime]
4. Gradleのマルチプラットフォーム設定
(1) ビルド.gradle.kts
KOTLIN
plugins {
kotlin("multiplatform") version "1.9.22"
}
group = "com.order"
version = "1.0.0"
repositories {
mavenCentral()
}
kotlin {
// Declare target platforms
jvm {
compilations.all {
kotlinOptions.jvmTarget = "17"
}
testRuns["test"].executionTask.configure {
useJUnitPlatform()
}
}
iosX64()
iosArm64()
iosSimulatorArm64()
js(IR) {
browser()
nodejs()
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val jvmMain by getting
val jvmTest by getting
val iosMain by getting
val jsMain by getting
}
}
5. 共有モジュール戦略
(1) 共有すべきこと、共有すべきでないこと
| レイヤー | 共有戦略 | 理由 |
|---|---|---|
| データモデル | ✅ 完全共有 | 純粋なデータ、プラットフォームに依存しない |
| ビジネスロジック | ✅ 完全に共有 | コアルールは一貫していなければならない |
| ネットワーク | ✅ 共有インターフェース + HTTP 対応 | インターフェースは統一されているが、実装は異なる |
| シリアライズ | ✅ 共有 (kotlinx.serialization) | マルチフォーマットのネイティブサポート |
| UI | ❌ プラットフォーム固有 | UIフレームワークには大きな違いがある |
| データベース | ⚠️ 共有SQL / expect | SQLは共有可能、ドライバは異なる |
| ロギング | ⚠️ 予想値/実測値 | プラットフォームごとのロギングAPIは異なる |
(2) 共通の戦略パターン
KOTLIN
// Pattern 1: Pure shared code (no expect/actual needed)
data class Order(val id: String, val total: Double, val status: String)
class OrderValidator {
fun validate(order: Order): List<String> = buildList {
if (!order.id.startsWith("ORD-")) add("Invalid ID format")
if (order.total < 0) add("Negative total")
if (order.status !in validStatuses) add("Invalid status")
}
companion object {
private val validStatuses = setOf("PENDING", "CONFIRMED", "SHIPPED", "CANCELLED")
}
}
// Pattern 2: Interface + expect factory
interface HttpClient {
suspend fun get(url: String): String
suspend fun post(url: String, body: String): String
}
expect fun createHttpClient(): HttpClient
// Pattern 3: expect function for platform-specific behavior
expect fun logDebug(tag: String, message: String)
6. 相互運用性
(1) プラットフォーム間の相互運用性に関する手法
| プラットフォームの組み合わせ | 相互運用性 | 備考 |
|---|---|---|
| JVM ↔ Java | シームレスな双方向通信 | KotlinからJavaを直接呼び出せる;JavaからKotlinを呼び出せる |
| iOS ↔ Swift | 双方向 | KotlinはObjective-Cフレームワークにコンパイルされ、Swiftからシームレスに呼び出し可能 |
| JS ↔ JavaScript | 双方向 | js() JS を呼び出す;@JsExport Kotlin をエクスポートする |
| Wasm ↔ JS | 片方向(JSからKotlinを呼び出す) | WasmモジュールはJSからの呼び出し用に関数をエクスポートする |
(2) JVMの相互運用性の例
KOTLIN
// Kotlin calling Java
val order = JavaOrderService() // Java class
order.processOrder("ORD-001") // Java method
// Java calling Kotlin (generated bytecode is standard)
// OrderKt.processOrder(order); // Top-level function
(3) JSの相互運用性の例
KOTLIN
// Kotlin calling JavaScript
fun fetchFromApi(url: String): dynamic {
return js("fetch(url).then(r => r.json())")
}
// Export Kotlin to JavaScript
@JsExport
class OrderValidator {
fun validate(id: String, total: Double): Boolean {
return id.startsWith("ORD-") && total >= 0
}
}
7. 完全な例:クロスプラットフォームのOrderValidator
▶ サンプル:クロスプラットフォームのOrderValidator
KOTLIN
// ============================================
// OrderProcessor - KMP Shared Module
// Feature: Shared OrderValidator with platform logging
// ============================================
// --- commonMain ---
data class Order(val id: String, val total: Double, var status: String, val customer: String)
// expect: platform-specific declaration
expect fun logInfo(tag: String, message: String)
class OrderValidator {
fun validate(order: Order): ValidationResult {
val errors = mutableListOf<String>()
if (!order.id.startsWith("ORD-")) errors.add("Invalid ID format: ${order.id}")
if (order.total < 0) errors.add("Negative total: ${order.total}")
if (order.total > 1_000_000) errors.add("Total exceeds maximum: ${order.total}")
if (order.status !in VALID_STATUSES) errors.add("Invalid status: ${order.status}")
val result = if (errors.isEmpty()) ValidationResult.Valid else ValidationResult.Invalid(errors)
logInfo("OrderValidator", "Validated ${order.id}: $result")
return result
}
fun calculatePriority(order: Order): String = when {
order.total > 10_000 -> "HIGH"
order.total > 1_000 -> "MEDIUM"
else -> "LOW"
}
companion object {
private val VALID_STATUSES = setOf("PENDING", "CONFIRMED", "SHIPPED", "DELIVERED", "CANCELLED")
}
}
sealed class ValidationResult {
object Valid : ValidationResult()
data class Invalid(val errors: List<String>) : ValidationResult()
}
// --- jvmMain ---
// actual fun logInfo(tag: String, message: String) {
// println("[$tag] $message") // Or use SLF4J
// }
// --- iosMain ---
// actual fun logInfo(tag: String, message: String) {
// NSLog("$tag: $message")
// }
// --- jsMain ---
// actual fun logInfo(tag: String, message: String) {
// console.log("[$tag] $message")
// }
// --- Demo (JVM target) ---
fun main() {
// Simulate JVM actual
// actual fun logInfo(tag: String, message: String) = println("[$tag] $message")
val validator = OrderValidator()
val orders = listOf(
Order("ORD-001", 299.99, "PENDING", "Alice"),
Order("BAD-002", -50.0, "INVALID", "Bob"),
Order("ORD-003", 15_000.00, "CONFIRMED", "Charlie"),
Order("ORD-004", 2_000_000.00, "PENDING", "Dave")
)
println("=== KMP OrderValidator Demo ===")
orders.forEach { order ->
val result = validator.validate(order)
val priority = validator.calculatePriority(order)
when (result) {
is ValidationResult.Valid -> println(" ✅ ${order.id}: Valid (Priority: $priority)")
is ValidationResult.Invalid -> println(" ❌ ${order.id}: ${result.errors}")
}
}
}
出力:
TEXT
📖 参照専用
=== KMP OrderValidator Demo ===
✅ ORD-001: Valid (Priority: LOW)
❌ BAD-002: [Invalid ID format: BAD-002, Negative total: -50.0, Invalid status: INVALID]
✅ ORD-003: Valid (Priority: HIGH)
❌ ORD-004: [Total exceeds maximum: 2000000.0]
❓ よくある質問
Q KMPとFlutterの違いは何ですか?
A KMPはビジネスロジックを共有し(プラットフォームごとのネイティブUI)、FlutterはUIを共有します(Skiaによるレンダリング)。 KMPは柔軟性が高く(ネイティブなUI体験を維持)、Flutterは統一性が高い(単一のUI)という特徴があります。両者は互いに補完し合うことができます。
Q KMPは本番環境での運用に耐えられますか?
A JVMおよびAndroid向けターゲットは完全に安定しており、iOS向けターゲットも安定しています。JSおよびWasm向けターゲットは急速に成熟しつつあります。Netflix、VMware、Cash Appなどの企業では、すでに本番環境で大規模に導入・運用されています。
Q 「expect」や「actual」はクラスでも使用できますか?
A はい。
expect class はクロスプラットフォームのインターフェースを定義しており、actual class はプラットフォームごとの実装を提供します。コンストラクタおよびメソッドのシグネチャは完全に一致している必要があります。Q 共有モジュールとプラットフォーム固有のモジュールはどのように構成すべきですか?
A 共通ロジックは
commonMain に配置し、プラットフォーム間の差異は境界部分で expect/actual を用いて分離してください。 commonMain内でのプラットフォーム固有のコードは避け、expectを使用してそれを抽象化してください。Q KMPはiOS開発者にとって使いやすいですか?
A 非常に使いやすいです。KMPはObj-Cフレームワークにコンパイルされ、Swiftからシームレスに呼び出すことができます。iOS開発者はSwift側の処理に集中すればよく、Kotlinの知識は必要ありません。
Q KMPのビルド速度はどの程度ですか?
A マルチターゲットのコンパイルでは、ビルド時間が長くなります(各ターゲットが個別にコンパイルされるため)。GradleのインクリメンタルコンパイルやKotlinコンパイラのキャッシュ機能を利用することで、この影響を軽減できます。CI/CD環境では、ターゲットの並列ビルドが推奨されます。
📖 まとめ
- KMPのコアアーキテクチャ:
commonMain(共有)+プラットフォーム固有のソースセット(jvmMain、iosMain、jsMain、…) expectはクロスプラットフォームの要件を定義し、actualはプラットフォームごとの実装を提供する- データモデルとビジネスロジックは完全に共有されており、UIおよびプラットフォームAPIでは
expect/actualが使用されています。 - Gradle
kotlin("multiplatform")はマルチターゲットビルドを宣言する - プラットフォーム間の相互運用性:JVM↔Javaはシームレス、iOS↔SwiftはObj-Cフレームワーク経由、JS↔JSは
js()/@JsExport経由 - KMPは「すべてか、まったくか」というものではなく、一部のモジュールだけを共有することで、段階的に導入することができます。
📝 練習問題
- 初心者 (⭐): KMP プロジェクトを作成し、
commonMain内でOrderを定義し、JVM および JS ターゲットでそれを使用します。ヒント:kotlin("multiplatform")Gradle プラグイン - 中級 (⭐⭐):
expect/actualを使用して、JVM と JS でgetPlatformName()が異なる値を返すようにします。ヒント:expect fun getPlatformName(): Stringとactualの実装 - 課題 (⭐⭐⭐): クロスプラットフォームのHTTPクライアントを実装する:
commonMain内でHttpClientインターフェースを定義し、jvmMain内でjava.net.URLおよびfetchAPI を使用してjsMainで実装する。ヒント:expect fun createHttpClient(): HttpClient