Kotlin: Kotlinマルチプラットフォーム入門

最終更新:2026-08-26

KMPはKotlinの究極のビジョンです。CharlieのOrderValidatorは一度記述するだけで、JVMバックエンド、iOSアプリ、JSフロントエンド、Wasmブラウザで再利用されます。expect/actualの仕組みにより、プラットフォーム間の違いは境界部分に限られます。

1. 学習内容


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マルチプラットフォームアーキテクチャ

100%
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環境では、ターゲットの並列ビルドが推奨されます。

📖 まとめ


📝 練習問題

  1. 初心者 (⭐): KMP プロジェクトを作成し、commonMain 内で Order を定義し、JVM および JS ターゲットでそれを使用します。ヒント: kotlin("multiplatform") Gradle プラグイン
  2. 中級 (⭐⭐): expect/actual を使用して、JVM と JS で getPlatformName() が異なる値を返すようにします。ヒント: expect fun getPlatformName(): Stringactual の実装
  3. 課題 (⭐⭐⭐): クロスプラットフォームのHTTPクライアントを実装する:commonMain内でHttpClientインターフェースを定義し、jvmMain内でjava.net.URLおよび fetch API を使用して jsMain で実装する。ヒント:expect fun createHttpClient(): HttpClient

← 前へ | 次へ →

Web-Tutorial.com

Web-Tutorial 技術チーム

複数の開発者によって共同維持されているプログラミングチュートリアルプラットフォーム。各チュートリアルは専門分野の開発者が執筆・レビューしています。正確で信頼性の高いコンテンツを目指しています — 問題を見つけた場合はお知らせください。

100%