# Kotlin Multiplatform 2026: AndroidとiOSでコード共有 > 2026年のKotlin MultiplatformがAndroidとiOSでビジネスロジックを共有する方法を解説します。Gradle設定、expect/actual、Ktor、Compose Multiplatform、KMPの面接質問までを網羅します。 - Published: 2026-06-26 - Updated: 2026-07-06 - Author: SharpSkill - Tags: Kotlin Multiplatform, KMP, Android, iOS, Cross-platform - Reading time: 10 min --- Kotlin Multiplatform (KMP) は、1つのKotlinコードベースでAndroidとiOS両方のアプリのビジネスロジックを動かしつつ、各プラットフォームが完全にネイティブなUI、パフォーマンス、SDKへのアクセスを維持できる技術です。2026年において、KMPはもはや実験段階ではありません。安定版であり、[Google](https://developer.android.com/kotlin/multiplatform) が公式に支援し、Netflix、McDonald's、Forbes、Cash App の本番環境で稼働しています。本稿では、コード共有が実際にどのように機能するのか、共有モジュールをどう構成するのか、そしてKMPの面接に臨む前に理解しておくべきトレードオフを詳しく解説します。 > **KMPが実際に共有するもの** > > Kotlin Multiplatform が共有するのはコンパイル済みのロジックであり、ランタイムではありません。共通のKotlinは、Android向けにJVMバイトコードへ、iOS向けに (LLVM経由で) ネイティブバイナリへコンパイルされます。JavaScriptブリッジも、埋め込みWebViewも、リフレクション層も存在しないため、共有コードは両プラットフォームでネイティブと同じ速度で動作します。 ## ネイティブパフォーマンスを犠牲にせずKotlin Multiplatformがコードを共有する仕組み 中心的な仕組みは、ソースセットの階層構造です。`commonMain` には、ネットワーク処理、シリアライズ、ビジネスルール、ビューモデルといったプラットフォーム非依存のKotlinを配置します。`androidMain` や `iosMain` のようなプラットフォーム別ソースセットには、プラットフォームAPIに触れるコードを配置します。コンパイラは、Android向けに `commonMain` と `androidMain` からJVMアーティファクトを、iOS向けに `commonMain` と `iosMain` からネイティブフレームワークを生成します。 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` という名前のフレームワークとして公開します。 ```kotlin // shared/build.gradle.kts 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の公式ドキュメント](https://kotlinlang.org/docs/multiplatform.html) にまとまっています。 ## expectとactualでプラットフォーム固有のコードを書く 一部のロジックは、プラットフォームAPIを必要とするため `commonMain` に置けません。`expect`/`actual` の仕組みは、共通コードで形状を宣言し、ターゲットごとに具体的な実装を供給します。解決はコンパイル時に行われ、ランタイムコストはゼロです。 ```kotlin // commonMain/kotlin/Platform.kt // 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` は自分がどちらと対話しているかを知ることはありません。 ```kotlin // androidMain/kotlin/Platform.kt 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 } ``` ```kotlin // iosMain/kotlin/Platform.kt 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全体に対して型付きのバインディングを同梱しているため、`UIDevice`、`NSUserDefaults`、Foundationの型は、手書きの繋ぎコードなしにKotlinから呼び出せます。 ## ネットワークとシリアライズのロジックを共有する ネットワーク処理は、API契約が両プラットフォームで同一であるため、共有する価値が最も高いコードです。1つの [Ktor](https://ktor.io/docs/) クライアントと `kotlinx.serialization` が、2つの並行実装を置き換えます。`suspend` 関数は [Kotlinコルーチン](/blog/android/mastering-kotlin-coroutines) に依存しており、両サイドで `async`/`await` へきれいに対応します。 ```kotlin // commonMain/kotlin/data/InterviewRepository.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 // 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 = client.get("https://api.sharpskill.dev/questions/" + tech).body() } ``` このリポジトリ、そのリトライロジック、そしてデータモデルは一度だけ書かれます。パーサーで修正されたバグは、AndroidとiOSで同時に修正されます。ここがKMPが導入コストを回収する部分です。同じパターンは、SQLDelightによる共有データベース、共有キーバリューストレージ、Koinによる共有依存性注入にも広がるため、データ層全体をUIが依存するインターフェースの背後で `commonMain` に置くことができます。 よくある間違いは、早すぎる段階で多くを共有しようとすることです。現実的な進め方は、まず1つのリポジトリから始め、両プラットフォームでビルドとCIパイプラインを実証し、それから自信が深まるにつれて機能を1つずつ移していくことです。 ## Compose Multiplatformで共有UIを構築する Compose Multiplatformは、2025年5月にiOSで安定版に到達し (バージョン1.8.0)、2026年半ばには1.11.0に達しました。ネイティブのテキスト入力と、デフォルトで有効な並行レンダリングを備えています。`commonMain` にある `@Composable` は両プラットフォームでレンダリングされ、共有ビューモデルは [MVVMまたはMVIアーキテクチャ](/blog/android/mvvm-vs-mvi-architecture) と自然に組み合わさります。 ```kotlin // commonMain/kotlin/ui/QuestionList.kt 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) { LazyColumn { items(questions) { question -> Card(modifier = Modifier.padding(8.dp)) { Text(text = question.prompt, modifier = Modifier.padding(16.dp)) } } } } ``` UIの共有は任意で、画面単位です。チームは [Compose Multiplatform](https://github.com/JetBrains/compose-multiplatform) でインターフェース全体を共有することも、アプリ全体をSwiftUIのままにしてロジックだけを共有することも、両者を混在させることもできます。ウィジェットやApp Clipのようにプラットフォーム固有のUXに依存する画面は、通常ネイティブのまま残します。 ## iOSでSwiftから共有コードを利用する 共有モジュールは、Swiftがあらゆるライブラリと同じようにインポートするフレームワークにコンパイルされます。歴史的な摩擦は、Kotlinの `suspend` 関数と `Flow` が扱いにくいコールバックAPIとして表出することでした。Touchlabの [SKIE](https://skie.touchlab.co/) はこれを解決し、慣用的なSwiftを生成します。`suspend` はネイティブの `async`/`await` に、`Flow` は `AsyncSequence` になります。 ```swift // iosApp/ContentView.swift 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面接対策](/technologies/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が `suspend` と `Flow` をネイティブの `async`/`await` と `AsyncSequence` として利用できるようにします。 --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/ja/blog/android/kotlin-multiplatform-2026