Kotlin Multiplatform (KMP)
導入背景と現状
KMPは2021年からWantedly アプリに本格導入され、現在はビジネスロジックの共有において中核的な役割を果たしています。 導入の経緯については、React Nativeをやめる話とKotlin Multiplatformで詳しく解説されています。
現在の導入状況
KMPはWantedly アプリに導入され、iOS / Android間でビジネスロジック(Reactor実装、API通信、データベース操作など)を共有しています。 2025年12月に、それまで別々だったリポジトリを単一のモノレポ wantedly/visit-app に統合しました。
リポジトリ構成
visit-app/
├── shared/ # iOS / Androidで共有するKMPコード(旧 visit-app-shared)
├── iosApp/ # iOSアプリ
└── androidApp/ # Androidアプリモノレポ化による改善
KMPの変更がアプリに反映されるまで20分以上かかっていたのが、即座に反映されるようになった
破壊的変更を同一PR内で検知できるようになった(従来は別リポジトリのCI実行まで気づけなかった)
iOS / Androidの変更を1つのPRでアトミックに行えるようになった
モノレポ化の詳細は Wantedly のモバイルアプリのモノレポ化について書かれた記事 を参照してください。
KMPの方針
Wantedly アプリ: 積極的な導入を推進
新機能開発時はKMP Reactorの採用を優先
既存機能の段階的な移行を実施
全社展開: 慎重な検討段階
認証処理などの共通性が高い機能から実験的に導入
各アプリの特性を考慮した段階的な適用
KMPのアーキテクチャ
設計思想
KMPでは、既存のReactorKitパターンを踏襲した独自のReactorアーキテクチャを採用しています。 これにより、iOS / Android開発者の学習コストを最小限に抑えながら、プラットフォーム間でのビジネスロジック共有を実現しています。

モジュール構成
shared の主要モジュール
:api: API通信関連(REST/GraphQL):entity: APIエンティティ定義:graphql: GraphQL クライアント(Apollo)
:core: 共通ユーティリティ:visit-app-shared: メインモジュール(Reactor実装):testing: テスト用ユーティリティ
技術スタック
データベース: SQLDelight(型安全なSQL)
API通信: Ktor Client + Apollo GraphQL
非同期処理: Kotlin Coroutines
依存性注入: 手動DI(DIフレームワークには依存しない)
テスト: Kotest
iOS連携: SKIE(Kotlin の共有コードを Swift から自然に扱えるようにする相互運用ツール)
iOS 連携(SKIE)
Wantedly アプリでは SKIE を導入し、KMP の共有コードを Swift にとって自然な形で公開しています。主に次の用途で活用しています。
sealed class の網羅的な switch: Swift 側で全ケースを網羅的に分岐でき、分岐漏れをコンパイル時に検出できる
suspend 関数の async/await 化: Kotlin の
suspend関数を Swift の async/await として自然に呼び出せるデフォルト引数・enum のブリッジ: Kotlin のデフォルト引数や enum を Swift から自然に扱える
Reactor
概要
ReactorはReactorKitを参考に開発された、KMP環境での単方向データフローを実現するフレームワークです。 iOS / Android両プラットフォームで一貫したアーキテクチャパターンを提供し、予測可能な状態管理を実現します。
データフロー

基本的なデータフロー
Action: UIからのユーザーインタラクション(タップ、入力など)
Mutate:
Actionを受けてAPI通信などの副作用を実行し、Mutationを生成Reduce:
Mutationを受けて現在のStateを新しいStateに変換State: UIが監視する状態オブジェクト、変更時にUI更新をトリガー
State の設計
State は 2 種類の情報で構成します。
モードに関わらず常に持つ情報(例: 一時的に表示するエラートースト)→ data class のフィールドにする
画面のモード(Loading / Succeeded / Error のいずれか一つ)→ sealed class にし、各モードに固有のデータはそのサブタイプに持たせる
shared(KMP)で定義するのは State だけです。State を UI に反映する処理は、後述のとおり iOS / Android の各プラットフォーム側が担います。
下記は、ある一覧画面(会社一覧など)を例にした最小の State 定義です。
hasNextPage / isNextPageLoading / nextPageCursor は成功後にしか意味を持たない情報なので、Loading や Error からは触れないよう Succeeded の中に閉じ込めています。追加読み込みは「新しい画面モードに遷移する」のではなく、Succeeded のまま copy でフラグを更新することで表現します。
なぜ sealed class で分けるのか
網羅性の強制: Kotlin の
when/ Swift のswitchが全サブタイプの分岐を要求するため、状態を追加したときの「対応漏れ」がコンパイルエラーで検出できる。不正な状態アクセスの排除:
Succeededだけが一覧データを、Errorだけが原因を持つため、「ローディング中なのに一覧を参照する」といったアクセスを型レベルで防げる。Declarative UI との相性: 「状態 → UI」を宣言的な一対一マッピングとして書ける。
なお「追加読み込み中」のような成功状態の中の細かな進捗は、新たなサブタイプを増やすのではなく Succeeded のフィールド(フラグや cursor)として持たせることで、状態の数を最小限に保っています。
実装詳細
詳細な実装については以下を参照:
wantedly/visit-app リポジトリ(
shared/配下に KMP の共有コード)
Database as Single Source of Truth
設計原則
KMPアーキテクチャでは、SQLDelightを使用したデータベースをSingle Source of Truthとして採用しています。 この設計により、データの一貫性を保ちながら、複数画面間でのリアルタイムな状態同期を実現しています。
技術実装
SQLDelight の特徴
型安全性: SQLクエリのコンパイル時検証
マルチプラットフォーム: iOS / Android共通のDB操作
Reactive: Kotlin Coroutines Flowによるリアクティブクエリ
パフォーマンス: ネイティブSQLiteの性能を活用
データフロー例:ブックマーク機能
依存関係

各画面のReactorは、SQLDelightのFlowを通じてデータベースの変更を監視します。 破線は、データの購読(Observe)関係を表しています。
データストリーム

初期データ取得: 各画面が必要なデータをDBから取得
リアルタイム監視: DBの変更をFlowで監視
自動UI更新: データ変更時に自動的にUI更新
状態変更フロー

ユーザーアクション: 詳細画面でブックマークボタンをタップ
API通信: サーバーにブックマーク状態を送信
DB更新: レスポンス受信後、ローカルDBを更新
自動伝播: DB変更が全ての購読者(画面)に自動通知
UI反映: 一覧画面と詳細画面の両方で即座にUI更新
実装上の利点
データ整合性: 単一のデータソースによる一貫した状態管理
パフォーマンス: ローカルDBによる高速なデータアクセス
オフライン対応: ネットワーク状況に依存しない基本機能
開発効率: プラットフォーム間でのロジック共有
GraphQL統合
導入背景 Wantedlyでは開発効率とパフォーマンス向上のためにGraphQLを採用しており、新しい画面は基本的にGraphQLで実装しています。 詳細はGraphQL Gateway - アプリ向けにAPIを公開するを参照してください。
KMPでの実装
クライアント: Apollo GraphQL Kotlin版
コード生成: GraphQLスキーマからKotlinコードを自動生成
型安全性: クエリとレスポンスの型安全な操作
キャッシュ: Apollo Clientの自動キャッシュ機能
開発方針
新機能: GraphQLを優先的に採用
既存機能: REST APIから段階的に移行
パフォーマンス: 必要なデータのみを取得(Over-fetching回避)
話を聞きに行きたい
Slack: #mobile_chapter
もっと知りたい
モバイルネイティブアプリについて の章(本 Handbook 内。各 OS の世代の全体像)
iOS の章 / Android の章(本 Handbook 内。各プラットフォームでの利用)
wantedly/visit-app リポジトリ (internal。KMP の共有コードを含む)
最終更新