プロダクション環境でのKotlin Multiplatform (KMP):Android、iOS、Web間でロジックを共有するためのガイド

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Kotlin Multiplatform導入から6ヶ月:プロダクション運用の振り返り

6ヶ月前、私たちのチームはメンテナンスの限界に直面していました。フィンテックアプリ用に、Android (Kotlin)、iOS (Swift)、Webダッシュボード (TypeScript) という3つの独立したコードベースを管理していたのです。金利計算ロジックが変更されるたびに、3つの異なるリポジトリを更新する必要がありました。

その結果、必然的に3種類のバグが発生し、QA(品質保証)の工数は3倍に膨れ上がりました。これを解決するため、コアとなるビジネスロジックをKotlin Multiplatform (KMP) に移行しました。プロダクション環境で半年運用した結果、開発速度は約40%向上しました。

UIスタック全体を制御しようとするFlutterやReact Nativeとは異なり、KMPは「本当に重要な部分」、つまりロジックの共有に焦点を当てています。iOSにはSwiftUI、AndroidにはJetpack ComposeといったネイティブUIを維持しながら、データモデル、ネットワーク、バリデーションロジックを一度書くだけで済みます。エンジニアの人数を毎年倍増させることなくモバイル製品をスケールさせたいなら、KMPの習得はもはや避けては通れません。

クイックスタート:5分で最初のプロジェクトを立ち上げる

以前のKMPのセットアップは退屈な手作業でしたが、エコシステムは成熟しました。現在はJetBrainsのウェブウィザードが, 面倒な設定作業を代行してくれます。

1. 環境チェック

Android StudioとXcodeが必要です。コードを書く前に、kdoctor をインストールしましょう。このツールは、ビルドエラーの原因となるCocoaPodsの欠落や特定のJavaバージョンなどを特定してくれます:

brew install kdoctor
kdoctor

出力結果にすべて緑色のチェックマークが表示されれば、あなたのマシンはマルチプラットフォーム開発の準備が完了しています。

2. プロジェクトの生成

kmp.jetbrains.com にアクセスします。Android、iOS、Web (Wasm) を選択してください。プロジェクトをダウンロードしてAndroid Studioで開くと、主に3つのディレクトリが表示されます:

  • composeApp/: 共有UIコード(Compose Multiplatformを使用する場合)。
  • iosApp/: 共有ロジックを利用する標準的なXcodeプロジェクト。
  • shared/: クロスプラットフォームのビジネスロジックが配置される engine room。

3. アプリの実行

「composeApp」構成を選択して、Androidエミュレーターで実行します。iOSの場合は、KMPプラグインを使用してAndroid Studioから直接起動できます。または、Xcodeで iosApp/iosApp.xcworkspace を開き、実機のiPhoneでデバッグすることも可能です。

「Expect/Actual」メカニズム:プラットフォーム間のギャップを埋める

KMPは、すべてのプラットフォームで共通する機能しか使えないような「最小公約数」のサンドボックスにあなたを閉じ込めたりしません. ファイルシステムやデバイスUUIDなど、プラットフォーム固有のAPIにアクセスする必要がある場合は、expect/actual パターンを使用します。これがKMPの柔軟性の秘訣です。

期待(Expectation)の定義

commonMain フォルダ内で、必要なインターフェースを定義します:

// shared/src/commonMain/kotlin
expect fun getPlatformName(): String

実体(Reality)の実装

コンパイラは、プラットフォーム固有のフォルダで実装を提供することを要求します。これにより、コンパイル時に正しいコードが効果的に注入されます。

// androidMain
actual fun getPlatformName(): String = "Android ${android.os.Build.VERSION.SDK_INT}"

// iosMain
import platform.UIKit.UIDevice
actual fun getPlatformName(): String = UIDevice.currentDevice.systemName()

ネットワークとシリアライゼーション

プロダクションレベルのアプリには、HTTPリクエストには Ktor、JSONには kotlinx.serialization を推奨します。この組み合わせにより、どこでも動作する単一のAPIクライアントを作成できます。リポジトリを commonMain で定義することで、Android、iOS、Webアプリで同一のネットワークロジックとデータモデルを共有できます。これだけで、最初の四半期にロジック関連のバグが30%減少しました。

応用編:状態管理とWeb (Wasm)

ネットワークの共有も素晴らしいですが、「ViewModel」のロジックを共有することで真のメリットが得られます。Kotlin CoroutinesFlow を使用することで、中央集権的なStateHolderを作成できます。

Flowによる状態の共有

私たちは、UIが共有状態をそのまま反映するだけの「dumb(単純)」なものになるよう、リアクティブなUI Stateパターンを使用して共有ロジックを構築しています。

class SharedViewModel {
    private val _state = MutableStateFlow(UiState())
    val state: StateFlow<UiState> = _state.asStateFlow()

    fun loadData() {
        // コルーチンのロジックをここに記述
    }
}

AndroidではJetpack Composeを介してこれを簡単に利用できます。iOSでは、SwiftUIの ObservableObject 内でこれらのFlowを収集(collect)できます。Webの場合、Kotlin/Wasmを使用すると、この同じコードをWebAssemblyにコンパイルできます。これにより、従来のJavaScriptブリッジのオーバーヘッドを回避し、ブラウザ上でネイティブに近いパフォーマンスを実現できます。

現場からの実践的なアドバイス

大規模アプリの移行から学んだのは、KMPには戦略的なアプローチが必要だということです。KMPは特効薬(シルバーバレット)ではありません。共有モジュールの過剰な設計(オーバーエンジニアリング)は避けるべきです。

1. UIを抽象化しすぎない

すべてのボタンやアニメーションを共有したくなるかもしれません。しかし、複雑なUIアニメーションは、ネイティブで書く方が簡単な場合が多いことがわかりました。**ロジック、データ、状態**を共有し、ピクセルの描画は適切な場合にはプラットフォーム固有のUIフレームワークに任せましょう。

2. SKIEでSwiftとの相互運用性を改善する

SwiftはKotlinのSealed ClassやFlowをネイティブに理解できないため、iPhoneシミュレーターでのKotlinコードのデバッグはストレスが溜まることがあります。そこで Touchlab SKIE プラグインを使用しましょう。これはSwiftフレンドリーなコードを生成し、共有されたKotlinモジュールをネイティブのSwiftライブラリのように扱えるようにしてくれます。これがないと、iOSエンジニアは扱いにくいジェネリック型に苦労することになるでしょう。

3. CI/CDとビルドコスト

ビルドパイプラインには、iOSターゲットをコンパイルするためのmacOSランナーが必要になります。GitHub ActionsのmacOSランナーは、Linuxランナーよりも大幅に高価であることに注意してください。私たちは、shared または iosApp ディレクトリに変更があったプルリクエストのみでiOSビルドチェックを実行するように最適化しました。

4. Expect/Actualの罠を避ける

独自の expect/actual コードを書くのは最終手段にしてください。KMPのエコシステムは広大です。ローカルデータベースに SQLDelight のようなライブラリがあるなら、それを使用しましょう。管理するプラットフォーム固有のコードが少ないほど、将来のプロジェクトのアップグレードが容易になります。

KMPの最初の学習曲線は約1週間です。しかし、長期的なメリットは計り知れません。たった一つのバグ修正がAndroid、iOS、Webに同時に反映されるのを見るのは、あらゆるエンジニアリングチームにとってゲームチェンジャーとなるはずです。

Share: