Kotlin Multiplatform 2026: AndroidとiOSでコード共有

2026年のKotlin MultiplatformがAndroidとiOSでビジネスロジックを共有する方法を解説します。Gradle設定、expect/actual、Ktor、Compose Multiplatform、KMPの面接質問までを網羅します。

2026年のKotlin MultiplatformでAndroidとiOSアプリ間のコードを共有する様子

Kotlin Multiplatform (KMP) は、1つのKotlinコードベースでAndroidとiOS両方のアプリのビジネスロジックを動かしつつ、各プラットフォームが完全にネイティブなUI、パフォーマンス、SDKへのアクセスを維持できる技術です。2026年において、KMPはもはや実験段階ではありません。安定版であり、Google が公式に支援し、Netflix、McDonald's、Forbes、Cash App の本番環境で稼働しています。本稿では、コード共有が実際にどのように機能するのか、共有モジュールをどう構成するのか、そしてKMPの面接に臨む前に理解しておくべきトレードオフを詳しく解説します。

KMPが実際に共有するもの

Kotlin Multiplatform が共有するのはコンパイル済みのロジックであり、ランタイムではありません。共通のKotlinは、Android向けにJVMバイトコードへ、iOS向けに (LLVM経由で) ネイティブバイナリへコンパイルされます。JavaScriptブリッジも、埋め込みWebViewも、リフレクション層も存在しないため、共有コードは両プラットフォームでネイティブと同じ速度で動作します。

ネイティブパフォーマンスを犠牲にせずKotlin Multiplatformがコードを共有する仕組み

中心的な仕組みは、ソースセットの階層構造です。commonMain には、ネットワーク処理、シリアライズ、ビジネスルール、ビューモデルといったプラットフォーム非依存のKotlinを配置します。androidMainiosMain のようなプラットフォーム別ソースセットには、プラットフォームAPIに触れるコードを配置します。コンパイラは、Android向けに commonMainandroidMain からJVMアーティファクトを、iOS向けに commonMainiosMain からネイティブフレームワークを生成します。

2026年のKotlin Multiplatformは、単一の信頼できる情報源から真に恩恵を受けるもの、つまりモデル、APIクライアント、キャッシュ、バリデーションの共有を重視します。UIはCompose Multiplatformで共有することも、画面単位でSwiftUIによる完全ネイティブのまま残すこともできます。この「ロジックは共有し、UIは選ぶ」というモデルこそが、既存アプリを書き直さずにKMPを採用できる理由です。

Kotlin Multiplatformは、1つの共有モジュールを複数のターゲット (JVM上のAndroid、ネイティブとしてのiOS、さらにデスクトップとWeb) にコンパイルするKotlin技術です。そのため、ビジネスロジックは一度書けばよく、各プラットフォームはネイティブUIと完全なSDKアクセスを保持します。現在Googleはこれを第一級の選択肢として支援しており、Room、DataStore、ViewModel、PagingといったJetpackライブラリがKMPアーティファクトを提供しています。

共有モジュールに何を含めるかを決めることは、KMPプロジェクトにおいて最も重要なアーキテクチャ上の判断です。以下の表は、2026年に多くの本番チームがどこで線引きするかを表しています。プレゼンテーション層より下はすべて共有の有力候補であり、一方でプラットフォームのUX慣習に触れるものは通常ネイティブのまま残します。

| レイヤー | commonMainで共有? | 理由 | |-------|---------------------|--------| | データモデルとDTO | はい | 両プラットフォームで同一のAPI契約 | | ネットワークとキャッシュ | はい | HTTPスタックは1つ、バグの表面積も1つ | | ビジネスルールとバリデーション | はい | ドメインはOSごとに変わらない | | ビューモデルと状態 | 通常は共有 | ComposeもSwiftUIも共有状態を利用する | | UIレンダリング | 任意 | Composeで共有、または画面ごとにネイティブ | | プラットフォームAPI (カメラ、生体認証) | いいえ | 代わりにexpect/actualで公開する |

得られる効果は、アプリがこの表の下半分をどれだけ占めるかに比例して大きくなります。UIが薄くデータ中心の製品ほど共有量が多く、高度にカスタマイズされアニメーション主導のアプリほど共有量は少なくなります。どちらの極端も間違いではありません。だからこそKMPは、全か無かの書き直しではなく段階的な導入に適しています。

Gradleで共有モジュールを構成する

KMPモジュールは、Kotlin DSLでターゲットとソースセットを宣言します。次の build.gradle.kts は、Androidと3つのiOSアーキテクチャ (実機、Intelシミュレータ、Apple Siliconシミュレータ) を設定し、iOSの出力を Shared という名前のフレームワークとして公開します。

shared/build.gradle.ktskotlin
plugins {
    kotlin("multiplatform")
    kotlin("plugin.serialization")
    id("com.android.library")
}

kotlin {
    // Android target compiles commonMain + androidMain to JVM bytecode
    androidTarget()

    // Three iOS targets: device, Intel sim, Apple Silicon sim
    listOf(iosArm64(), iosX64(), iosSimulatorArm64()).forEach { target ->
        target.binaries.framework {
            baseName = "Shared"   // becomes "import Shared" in Swift
            isStatic = true
        }
    }

    sourceSets {
        commonMain.dependencies {
            implementation("io.ktor:ktor-client-core:3.3.0")
            implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0")
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
        }
        androidMain.dependencies {
            implementation("io.ktor:ktor-client-okhttp:3.3.0")
        }
        iosMain.dependencies {
            implementation("io.ktor:ktor-client-darwin:3.3.0")
        }
    }
}

各ターゲットはプラットフォーム固有のKtorエンジン (Androidでは okhttp、iOSでは darwin) を取り込む一方、共有コードは共通の ktor-client-core APIのみに依存します。設定可能なオプションの全体像は Kotlin Multiplatformの公式ドキュメント にまとまっています。

expectとactualでプラットフォーム固有のコードを書く

一部のロジックは、プラットフォームAPIを必要とするため commonMain に置けません。expect/actual の仕組みは、共通コードで形状を宣言し、ターゲットごとに具体的な実装を供給します。解決はコンパイル時に行われ、ランタイムコストはゼロです。

commonMain/kotlin/Platform.ktkotlin
// commonMain declares what it needs but cannot implement here
expect class Platform() {
    val name: String
    val osVersion: String
}

// Shared logic can now depend on Platform freely
fun analyticsUserAgent(): String {
    val platform = Platform()
    return "SharpSkill/" + platform.name + " " + platform.osVersion
}

Android実装は Build クラスから、iOS実装は UIDevice から値を読み取ります。どちらも同じ契約を満たすため、commonMain は自分がどちらと対話しているかを知ることはありません。

androidMain/kotlin/Platform.ktkotlin
import android.os.Build

// actual supplies the Android-specific implementation
actual class Platform actual constructor() {
    actual val name: String = "Android"
    actual val osVersion: String = Build.VERSION.RELEASE
}
iosMain/kotlin/Platform.ktkotlin
import platform.UIKit.UIDevice

// actual uses UIKit directly, no Objective-C bridging code required
actual class Platform actual constructor() {
    actual val name: String = "iOS"
    actual val osVersion: String = UIDevice.currentDevice.systemVersion
}

Kotlin/NativeはiOS SDK全体に対して型付きのバインディングを同梱しているため、UIDeviceNSUserDefaults、Foundationの型は、手書きの繋ぎコードなしにKotlinから呼び出せます。

ネットワークとシリアライズのロジックを共有する

ネットワーク処理は、API契約が両プラットフォームで同一であるため、共有する価値が最も高いコードです。1つの Ktor クライアントと kotlinx.serialization が、2つの並行実装を置き換えます。suspend 関数は Kotlinコルーチン に依存しており、両サイドで async/await へきれいに対応します。

commonMain/kotlin/data/InterviewRepository.ktkotlin
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

// One serializable model, zero duplication across platforms
@Serializable
data class Question(val id: String, val prompt: String, val difficulty: String)

// One HttpClient, one JSON parser, one repository for both apps
class InterviewRepository {
    private val client = HttpClient {
        install(ContentNegotiation) { json() }
    }

    suspend fun fetchQuestions(tech: String): List<Question> =
        client.get("https://api.sharpskill.dev/questions/" + tech).body()
}

このリポジトリ、そのリトライロジック、そしてデータモデルは一度だけ書かれます。パーサーで修正されたバグは、AndroidとiOSで同時に修正されます。ここがKMPが導入コストを回収する部分です。同じパターンは、SQLDelightによる共有データベース、共有キーバリューストレージ、Koinによる共有依存性注入にも広がるため、データ層全体をUIが依存するインターフェースの背後で commonMain に置くことができます。

よくある間違いは、早すぎる段階で多くを共有しようとすることです。現実的な進め方は、まず1つのリポジトリから始め、両プラットフォームでビルドとCIパイプラインを実証し、それから自信が深まるにつれて機能を1つずつ移していくことです。

Androidの面接対策はできていますか?

インタラクティブなシミュレーター、flashcards、技術テストで練習しましょう。

Compose Multiplatformで共有UIを構築する

Compose Multiplatformは、2025年5月にiOSで安定版に到達し (バージョン1.8.0)、2026年半ばには1.11.0に達しました。ネイティブのテキスト入力と、デフォルトで有効な並行レンダリングを備えています。commonMain にある @Composable は両プラットフォームでレンダリングされ、共有ビューモデルは MVVMまたはMVIアーキテクチャ と自然に組み合わさります。

commonMain/kotlin/ui/QuestionList.ktkotlin
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Card
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.foundation.layout.padding

// This list renders identically on Android and iOS
@Composable
fun QuestionList(questions: List<Question>) {
    LazyColumn {
        items(questions) { question ->
            Card(modifier = Modifier.padding(8.dp)) {
                Text(text = question.prompt, modifier = Modifier.padding(16.dp))
            }
        }
    }
}

UIの共有は任意で、画面単位です。チームは Compose Multiplatform でインターフェース全体を共有することも、アプリ全体をSwiftUIのままにしてロジックだけを共有することも、両者を混在させることもできます。ウィジェットやApp Clipのようにプラットフォーム固有のUXに依存する画面は、通常ネイティブのまま残します。

iOSでSwiftから共有コードを利用する

共有モジュールは、Swiftがあらゆるライブラリと同じようにインポートするフレームワークにコンパイルされます。歴史的な摩擦は、Kotlinの suspend 関数と Flow が扱いにくいコールバックAPIとして表出することでした。Touchlabの SKIE はこれを解決し、慣用的なSwiftを生成します。suspend はネイティブの async/await に、FlowAsyncSequence になります。

iosApp/ContentView.swiftswift
import SwiftUI
import Shared   // the KMP framework, baseName = "Shared"

struct ContentView: View {
    @State private var questions: [Question] = []
    private let repository = InterviewRepository()

    var body: some View {
        List(questions, id: \.id) { question in
            Text(question.prompt)
        }
        .task {
            // With SKIE, the Kotlin suspend fun is a Swift async fun
            questions = (try? await repository.fetchQuestions(tech: "android")) ?? []
        }
    }
}

iOSチームは通常のSwiftとSwiftUIを書きます。彼らは共有モデルとリポジトリを、ネイティブのSwiftパッケージとまったく同じように利用するため、KMPはiOSコードベースの大部分から見えないままです。

面接で言及する価値のある粗い部分がまだ2つあります。1つ目はビルド時間です。Kotlin/Nativeのコンパイルは純粋なSwiftのビルドより遅いものの、Gradleのビルドキャッシュと事前ビルドされたXCFrameworkの配布が日々のコストを和らげます。2つ目は、SwiftからKotlinへの境界を越えたデバッグです。改善されてはいるものの、1つの言語にとどまる場合ほどまだシームレスではありません。これも、チームが共有範囲を無秩序に広げず意図的に保つもう1つの理由です。

予想されるKotlin Multiplatformの面接質問

KMPの面接質問はAndroidやモバイルの職種でますます登場するため、幅広い Android面接対策 と並行して、繰り返し出るいくつかを練習しておく価値があります。

  • expect/actualはインターフェースとどう違いますか? expect/actualは、ランタイムのディスパッチなしにターゲットごとにコンパイル時で解決され、クラス、関数、プロパティ、typealiasの裏付けとなれます。インターフェースはランタイムで動的に解決されます。プラットフォームAPIにはexpect/actualを、共有コード内の多態性にはインターフェースを使います。
  • Kotlin/Nativeはメモリとスレッドをどう扱いますか? Kotlin 1.7.20以降、新しいメモリマネージャが従来のオブジェクトフリーズモデルを廃止したため、共有される可変状態とコルーチンは、JVMとほぼ同じようにスレッドをまたいで動作します。
  • 既存アプリはKMPを段階的に採用できますか? はい。共有モジュールはAndroid向けにAAR、iOS向けにXCFrameworkとして配布されるため、書き直しなしで1つのリポジトリや機能を一度に移行できます。
  • Compose Multiplatformを使わずにUIをネイティブのまま残すべきなのはどんなときですか? 画面がプラットフォーム固有のUXに依存するとき、あるいはiOSチームがプレゼンテーション層を所有し自分で制御したいときです。
2026年のツールチェーン

K2コンパイラを備えたKotlin 2.3、ネットワーク処理にはKtor 3.x、共有データベースにはSQLDelight、依存性注入にはKoinをターゲットにします。iOSのインターオペには、早い段階でSKIEを追加します。Swift層を書いた後に後付けするのは、はるかに苦痛です。

まとめ

2026年のKotlin Multiplatformは、ネイティブUIやパフォーマンスを手放すことなく、AndroidとiOSにまたがる重複ロジックを削減する現実的な方法です。

  • 単一の信頼できる情報源を持つロジック (モデル、ネットワーク、バリデーション) を共有し、画面ごとにネイティブUIかCompose UIかを選びます。
  • プラットフォームAPIには expect/actual を使います。コンパイル時に解決され、ランタイムのオーバーヘッドはありません。
  • Ktorと kotlinx.serialization でネットワーク処理を一度だけ書けば、修正が両プラットフォームに同時に反映されます。
  • 書き直しにコミットするのではなく、AARとXCFrameworkを通じて段階的に採用します。
  • 最初からSKIEを追加し、Swiftが suspendFlow をネイティブの async/awaitAsyncSequence として利用できるようにします。

今すぐ練習を始めましょう!

面接シミュレーターと技術テストで知識をテストしましょう。

タグ

#Kotlin Multiplatform
#KMP
#Android
#iOS
#Cross-platform

共有

関連記事