一般社団法人 全国個人事業主支援協会

COLUMN コラム

  • Kotlin Multiplatformの現状と展望:共有ロジックの設計パターン

Kotlin Multiplatformが注目される背景

モバイルアプリケーション開発において、iOSとAndroidの両プラットフォームでビジネスロジックを共有したいというニーズは以前から存在していました。React NativeやFlutterがUIレイヤまで含めたクロスプラットフォーム開発を提供する中、Kotlin Multiplatform(KMP)はビジネスロジックの共有に焦点を当てた独自のポジションを確立しています。

筆者が実際にKMPをプロダクションに導入した経験から言えるのは、「UIはネイティブで書きたいが、ロジックの二重実装は避けたい」というチームにとって、KMPは最も現実的な選択肢だということです。

KMPのアーキテクチャ概要

KMPの基本的な考え方は、共通コード(commonMain)とプラットフォーム固有コード(iosMain、androidMain等)を明確に分離することです。共通コードはKotlinで記述し、プラットフォーム固有の処理はexpect/actualメカニズムで抽象化します。

// commonMain - 共通の定義
expect class PlatformLogger() {
fun log(message: String)
}

// androidMain - Android固有の実装
actual class PlatformLogger actual constructor() {
actual fun log(message: String) {
android.util.Log.d("KMP", message)
}
}

// iosMain - iOS固有の実装
actual class PlatformLogger actual constructor() {
actual fun log(message: String) {
platform.Foundation.NSLog(message)
}
}

このexpect/actualのパターンにより、共通コードからプラットフォーム固有のAPIを呼び出す際の抽象化が自然に行えます。

共有すべきレイヤの見極め

KMP導入で最も重要な判断は「どこまで共有するか」です。実務経験から、以下のレイヤ分けが効果的です。

  • 積極的に共有すべき:ドメインモデル、ビジネスルール、バリデーションロジック、APIクライアント、データ変換
  • 状況次第で共有:ViewModel層、ローカルデータベースアクセス、状態管理
  • 共有しないほうがよい:UIコンポーネント、プラットフォーム固有のUXパターン、OS固有の機能(通知、権限管理等)

特にAPIクライアントの共有は効果が高いです。Ktorライブラリを使えば、HTTPクライアントをマルチプラットフォームで共有できます。

// commonMain
class ApiClient(private val httpClient: HttpClient) {

suspend fun fetchUsers(): List<User> {
return httpClient.get("https://api.example.com/users")
.body<List<User>>()
}

suspend fun createUser(request: CreateUserRequest): User {
return httpClient.post("https://api.example.com/users") {
contentType(ContentType.Application.Json)
setBody(request)
}.body<User>()
}
}

@Serializable
data class User(
val id: Long,
val name: String,
val email: String
)

@Serializable
data class CreateUserRequest(
val name: String,
val email: String
)

状態管理の共有パターン

ViewModelレイヤの共有は議論が分かれるところですが、筆者のチームではKotlin Flowを活用した状態管理を共有コードに置いています。

// commonMain
class UserListViewModel(
private val apiClient: ApiClient
) {
private val _state = MutableStateFlow<UserListState>(UserListState.Loading)
val state: StateFlow<UserListState> = _state.asStateFlow()

fun loadUsers() {
_state.value = UserListState.Loading
CoroutineScope(Dispatchers.Default).launch {
try {
val users = apiClient.fetchUsers()
_state.value = UserListState.Success(users)
} catch (e: Exception) {
_state.value = UserListState.Error(e.message ?: "Unknown error")
}
}
}
}

sealed class UserListState {
object Loading : UserListState()
data class Success(val users: List<User>) : UserListState()
data class Error(val message: String) : UserListState()
}

Android側ではこのStateFlowをそのままJetpack Composeのcollectで受け取れますし、iOS側ではKMP-NativeCoroutinesなどのライブラリを使ってSwiftのasync/awaitやCombineに変換できます。

iOS連携の課題と対策

KMPの最大の課題は、iOS側との連携です。KotlinコードはObjective-Cのヘッダとして公開されるため、Swiftから利用する際にいくつかの制約があります。

  • ジェネリクスの制限:Kotlinのジェネリクスは一部がObjective-Cの型に正しくマッピングされない
  • Coroutineの扱い:suspend関数がCompletionHandlerベースのコールバックに変換される
  • Sealed classの扱い:exhaustive switchがSwift側で直接使えない
  • Enum classの制限:Kotlin EnumのプロパティやメソッドがiOS側で扱いにくい

これらの課題に対しては、SKIE(Swift Kotlin Interface Enhancer)やKMP-NativeCoroutinesといったライブラリが大きく改善してくれます。特にSKIEを導入すると、sealed classのSwift enumへのマッピングやCoroutineのasync/await対応が自動化されるため、iOS開発者の体験が大幅に向上します。

テスト戦略

KMPプロジェクトのテストは共通コードに集中させるのが効率的です。commonTestに記述したテストはすべてのターゲットプラットフォームで実行されるため、一度書いたテストがiOS・Androidの両方をカバーします。

// commonTest
class UserValidatorTest {

@Test
fun validEmailShouldPass() {
val validator = UserValidator()
assertTrue(validator.isValidEmail("user@example.com"))
}

@Test
fun invalidEmailShouldFail() {
val validator = UserValidator()
assertFalse(validator.isValidEmail("not-an-email"))
}
}

導入の判断基準

KMPの導入を検討する際は、チーム構成を最も重視すべきです。Android開発者がKotlinに精通していることは前提として、iOS開発者がKotlinコードの読解や共有モジュールへの貢献に前向きであるかが成否を分けます。技術的な障壁よりも、チーム間のコミュニケーションコストのほうが導入成功の鍵を握っているというのが実感です。

KMPは成熟度を着実に上げており、2025年現在ではプロダクション利用の実績も十分に蓄積されています。「まずはネットワーク層だけ共有する」という小さなスタートから始めるのが、リスクを抑えた現実的なアプローチです。

この記事をシェアする

  • Twitterでシェア
  • Facebookでシェア
  • LINEでシェア