expect/actual は、共通コードでプラットフォーム依存の API を宣言できる Kotlin Multiplatform のメカニズムです。 expect キーワードは commonMain で関数、クラス、またはプロパティのコントラクトを作成し、actual キーワードは各プラットフォームに対する具体的な実装を提供します。コンパイラは、すべてのターゲットプラットフォームで各 expect 宣言に対応する actual 実装が存在することを確認します。JetBrains, 2025によると、このメカニズムは 80% の KMM プロジェクトでプラットフォームビジネスロジックの実装に使用されています。
ポイント
expect/actual は、プラットフォーム向けプログラミングを実装するための Kotlin Multiplatform の宣言的メカニズムです。共通モジュールで一度 API を記述し (expect)、各プラットフォームに対して独立に実装できます (actual)。インターフェイスとは異なり、expect/actual は仮想コールを生成しません—コンパイラがコンパイル時に expect と actual の宣言を結合し、動的ディスパッチのオーバーヘッドを削除します。
expect/actual の歴史は、2017 年に Kotlin Multiplatform が登場したことに始まります。当初は expect/actual declarations と呼ばれ、実験的な段階でした。Kotlin 1.2 で expect 注釈が追加され、Kotlin 1.3 で expect/actual はクラスと関数に対して安定版となりました。時間とともにメカニズムは拡張されました: Kotlin 1.6 はコンパニオンオブジェクトに対する expect/actual サポートを追加し、Kotlin 1.7 は enum クラスに対して、Kotlin 2.0 は typealias に対して追加しました。
expect/actual の主な特徴は コンパイル時の安全性です。開発者が commonMain に expect 宣言を追加したが iOS に対する actual 実装を忘れた場合、コンパイラはエラーを発生します。これにより、リフレクションやプラットフォームコードの動的読み込みを使用するアプローチによく見られるランタイムエラーを防ぐことができます。
expect/actual の メカニズム は、source set レベルで動作します—Kotlin Multiplatform のモジュールシステムです。すべてのプラットフォームで利用可能な共通コードは commonMain source set に所在します。プラットフォーム依存コードは、iosMain、androidMain、macosMain などに所在します。commonMain の expect キーワードは API を宣言し、プラットフォーム source set の actual キーワードが実装を提供します。コンパイラはコード生成階段でこれらを結合し、ターゲットプラットフォームに対する expect 関数コールを対応する actual 実装に置き換えます。
情報システムの source set 階層は次のようになります: commonMain は expect 宣言を含み、iosMain と androidMain は actual 実装を含みます。iOS に対してコンパイルする場合は iosMain から actual が使用され、Android に対してコンパイルする場合は androidMain から actual が使用されます。Source set は中間的なものになることがあり (例えば、特定のアーキテクチャに対する iosArm64Main)、異なるデバイスに対して実装を完善化できます。
// commonMain — expect declaration
expect fun getPlatformName(): String
// androidMain — actual for Android
actual fun getPlatformName(): String = "Android"
// iosMain — actual for iOS
actual fun getPlatformName(): String = "iOS"
Kotlin コンパイラ は、expect/actual を扱う際にいくつかの条件を検証します。各 expect 宣言には、各アクティブなプラットフォームに対する actual 実装が必要です。actual 宣言のシグネチャは expect シグネチャと一致する必要があります (@OptionalExpectation 注釈はこの要件を緩和できます)。アクセス修飾子、戻り値の型、パラメータは同じである必要があります。また、expect と actual の宣言の間に循環依存がないことも確認します。
expect/actual はいくつかの宣言タイプをサポートしています。最もよく使われるのは、プラットフォーム操作のための expect/actual 関数、ネイティブ実装が必要なオブジェクトのための expect/actual クラス、および定数と設定のための expect/actual プロパティです。各タイプには、それぞれの使用ルールと制限があります。
Expect/actual 関数 は、最も簡単で一般的なタイプです。時間取得、ファイル読み込み、HTTP リクエスト送信などのプラットフォーム API を呼び出すために使用されます。Expect/actual クラス は、ネイティブコードと直接相互作用するオブジェクトを作成するために使用されます (例えば、カメラ、地理情報、鍵保管へのアクセスのため)。Expect/actual プロパティ (val) は、プラットフォームの定数に適しています—OS 名、SDK バージョン、システムディレクトリパスなど。
| 宣言タイプ | キーワード | 使用例 |
|---|---|---|
| 関数 | expect fun / actual fun | デバイス固有の識別子を取得する |
| クラス | expect class / actual class | SecureStorage へのアクセス (Keychain / EncryptedSharedPreferences) |
| プロパティ | expect val / actual val | 現在のプラットフォーム (iOS / Android) |
| Enum クラス | expect enum / actual enum | 利用可能なアプリ権限のリスト |
| Typealias | expect typealias / actual typealias | プラットフォーム固有のネットワーク応答タイプ |
すべての Kotlin 構造が expect/actual で使用できるわけではありません。expect 宣言 はボディを持つことができません—シグネチャのみです。expect クラスはパラメータを持つコンストラクタを持つことができません (空のプライマリコンストラクタが必要です)。enum expect/actual には、expect と actual の両方ですべての定数が同じである必要があります。Expect プロパティは val (ではなく var) である必要があります。なぜなら、プラットフォームプロパティに対して共通モジュールで状態を保存することは意味をなさないからです。
簡単な関数から完全なクラスまで、expect/actual の実践例を見てみましょう。基本箇所は、UI で使用するためにプラットフォーム名を取得することです。より複雑な例では、ネイティブストレージへのアクセスやプラットフォームスレッドの扱いが含まれます。
// commonMain — expect class for secure storage
expect class PlatformStorage {
fun save(key: String, value: String)
fun get(key: String): String?
fun remove(key: String)
}
// androidMain — actual on Android
actual class PlatformStorage {
private val prefs = AppContext.getSharedPreferences("secure", 0)
actual fun save(key: String, value: String) { prefs.edit().putString(key, value).apply() }
actual fun get(key: String): String? = prefs.getString(key, null)
actual fun remove(key: String) { prefs.edit().remove(key).apply() }
}
この例では、expect クラス PlatformStorage が簡単なキーバリューストレージのコントラクトを定義します。Android では実装が SharedPreferences を使用し、iOS では Keychain または NSUserDefaults を使用します。expect/actual により、commonMain のビジネスロジックはプラットフォーム実装を知らなくても save/get/remove を呼び出せます。
// iosMain — actual on iOS with Keychain
actual class PlatformStorage {
actual fun save(key: String, value: String) {
val query = mapOf<String, Any>(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to key,
kSecValueData to value.encodeToByteArray()
)
SecItemAdd(query, null)
}
actual fun get(key: String): String? {
val query = mapOf<String, Any>(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to key,
kSecReturnData to true
)
val result = mutableMapOf<String, Any>()
return if (SecItemCopyMatching(query, result) == errSecSuccess)
result[kSecValueData]?.toString()
else null
}
actual fun remove(key: String) {
val query = mapOf<String, Any>(
kSecClass to kSecClassGenericPassword,
kSecAttrAccount to key
)
SecItemDelete(query)
}
}
expect/actual API を設計する際は、いくつかの原則に従う必要があります。expect 宣言の数を 最小化 してください—共通コードが多いほど、メンテナンスが簡単になります。expect/actual は、プラットフォームで真に異なる API にのみ使用してください。それ以外のコードには、ファクトリーや依存性インジェクションを伴うインターフェイスを使用してください。これにより、テストが簡単になります。
expect 宣言は、げんちゃばげに テーマ別モジュール にグループ化することをおすすめします。例えば、Storage.kt はストレージに関する expect 宣言のため、Platform.kt は OS を扱う expect 関数のため、Analytics.kt はアナリティクスの expect クラスのためです。これにより、KMM プロジェクトのプラットフォーム表面のナビゲーションと理解が簡単になります。各 actual ファイルは、対応する source set にある必要があります: androidMain、iosMain、desktopMain など。
actual が共通コードを使用する expect fun と actual fun による デフォルト実装 は、よくあるパターンですが、反パターンです。プラットフォーム実装がデフォルトと違わない場合、expect/actual は不要です。そのような場合は、commonMain で簡単な関数を使用してください。また、単純なゲッターに expect/actual を使用するのを避けてください—定数とともに expect val を使用してください。
expect/actual コードの適切な構造は、プロジェクトの可読性にとって重要です。各 expect/actual モジュールには、単一のエントリポイントがある必要があります。組織例: commonMain/kotlin/com/project/platform は expect 宣言を含み、androidMain/kotlin/com/project/platform は Android に対する actual を含み、iosMain/kotlin/com/project/platform は iOS に対する actual を含みます。expect と actual でファイル名とパッケージ名を一致させる必要があります。これにより、開発者が対応する実装を高速に見つけられます。
プラットフォームファクトリーによる インターフェイス は、expect/actual の主な代替手法です。expect クラスの代わりに、commonMain でインターフェイスを宣言し、プラットフォームモジュールで具体的なクラスを作成できます。ファクトリーや依存性インジェクションコンテナが、ランタイムに正しい実装を提供します。このアプローチは、インターフェイスをモックできるため、テストに適しています。
依存性インジェクション (Koin, Kodein) は、より柔軟ですが効率の低いアプローチです。DI コンテナはプラットフォームごとに独立して構成され、共通コードにプラットフォーム依存を提供します。expect/actual とは異なり、インジェクションはランタイムに生じるため、テストのために実装を置換えます。一方で、DI 構成エラーはコンパイル時ではなく、ランタイムにのみ検出されます。
| アプローチ | コンパイル時の検証 | テストの柔軟性 | ランタイムオーバーヘッド |
|---|---|---|---|
| expect/actual | 完全 | 低 (actual はモックできない) | ゼロ (コンパイル時結合) |
| インターフェイス + ファクトリー | 部分的 | 高 (モック可能) | 最小 (仮想コール) |
| 依存性インジェクション | なし (ランタイム) | 高 | 中程度 (DI プロキシ) |
expect/actual と代替手法の選択はコンテキストに依存します。パフォーマンス重要なコード (ゲームエンジン、リアルタイム処理) には、オーバーヘッドがゼロなため、expect/actual が優先されます。ビジネスロジック (リポジトリ、ユースケース) には、テストを簡単にするために DI を伴うインターフェイスを使用するほうが良いでしょう。組み合わせアプローチ—低レベルのプラットフォーム操作には expect/actual、ビジネスロジック層にはインターフェイス—は、大多数のプロダクション KMM プロジェクトで使用されています。
よくある質問
expect/actual は仮想コールなしでコンパイル時に実装を結合し、インターフェイスはランタイムに結合します。expect/actual はすべてのプラットフォームに対する実装を保証し、インターフェイスはランタイム検査が必要です。
はい、expect enum は Kotlin 1.7 からサポートされています。expect と actual enum ですべての定数が一致する必要があります。異なるプラットフォームで定数の値が違うとコンパイルエラーになります。
コンパイラ は、actual 実装がない各プラットフォームに対してエラーを発生します。すべての expect 宣言に対応する actual 実装が追加されるまで、プロジェクトはビルドされません。
いいえ、expect と actual は異なる source set にある必要があります。expect は commonMain または中間 source set に、actual はプラットフォーム source set にあります。expect と actual を同じ source set に置くとコンパイルエラーになります。
expect/actual をテストするには、プラットフォームテスト source set を伴う commonTest を使用します。commonTest で expect テストを書き、各プラットフォームに actual テストを書きます。統合テストは、各ターゲットプラットフォームで単独で実行されます。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。