Share
Embed
なぜ私たちは「現時点では」KMPの導入を見送ったのか
Kotlin Fest 2026 での発表資料 https://2026.kotlinfest.dev/sessions/session-10
KMPは今やAndroid・iOS間でコードベースを共有しながら、十分なクオリティのアプリを作れるほど機能が充実し、プロダクション導入する事例を多く見聞きするようになりました。
私たちのアプリもAndroid・iOSを別コードベースで開発しており、開発効率向上を求めてKMP導入を検証しました。Swift Exportを中心に目まぐるしく追加される機能や、他社での運用事例を検証したうえで、「現時点では」KMP導入を見送るという結論に至りました。同時に「どうなったら再開するか」も検討し、Swift Exportの進化に求められる条件や、今のAndroid・iOSコードベース側で解消すべき技術的負債を具体的に言語化しています。
このセッションでは、Swift Exportの技術的な現在地にも触れながら、この一連の検討プロセスを皆さんのチームでもそのまま使える形で共有します。
Kazuki Chigita
November 09, 2026
Transcript
34 slides
-
なぜ私たちは「現時点では」 KMPの導 を 送ったのか Kotlin Fest @ fun - ホールA
Kazuki CHIGITA (@chigichan ) スライドはこちら speakerdeck.com/chigichan / how-we-decided-to-suspend-kmp-adoption 1 見 4 2 入 6 4 2 2 0 2 ©MIXI -
アイスブレーク 導 事例は多く共有されている で すべてのプロジェクトで本当にメリットはあるのか? 今2026年にKMPを 検討するには何をすれば良いのだろうか? KMPを活 し、コードが KMPの導
を コードの共有をしていない プラットフォームを 検討している 興味がある 共有化するまでのコストや時間って またいで共有されている どうなんだろうか? 段階的な移 のためには 何が必要なのだろうか? KMPを”選択しない”という選択肢は ないのだろうか? 5 方 一 ・ 行 入 用 入 ©MIXI -
Kazuki CHIGITA (@chigichan ) 好きなメソッドは firstNotNullOfOrNull 株式会社MIXIで「家族アルバム みてね」の開発を しています みてねではプロダクト開発メンバーを募集しています
https://team.mitene.us 6 4 2 ©MIXI 己 自 紹介 -
の20分で話すこと なぜKMPの検討を始めたか? Swift Exportを中 「 に検討して した判断軸 7 見 用
心 ©MIXI 見 えたこと 送る」ことをどう意思決定したか まとめ: 意思決定に使 日 2 1 3 4 今 -
私たちのコードベース それぞれ別のアーキテクチャで発展 レイヤー構造(アーキテクチャ)は似てきた • iOS • iOS • Android •
Android 画 ≈ 画 ドメイン ≈ ドメイン データ ≈ データ ただし細部はそれぞれ違う リリース当初 現在 似ている構造ならそのまま共通化できないか? 面 面 ©MIXI 1 なぜKMPの検討を始めたか? 9 -
メンバーのスキル ドメイン制のチーム 公開事例を KMP 事例 iOS Android Server (AI による
援以前から) ひとりで複数プラットフォームの 実装を担えるメンバーが多い iOS Android ドメインのメンバーで機能単位で 責任を持つので、 ずと少 数で iOS ↔ Android の同じ機能を 実装してきた マルチプラットフォームの導 で開発体験の改善が 実際に ロゴ素材:https://kotlinlang.org/docs/kotlin-brand-assets.html 見 行 入 人 見 自 見 支 なぜKMPの検討を始めたか? 行 1 ©MIXI 手 1 2 3 見 「マルチプラットフォーム」の期待だけが先 ると 私たちに ぴったり? JetBrains が出している事例を ると、私たちのチームに マッチしているように えた 込めると考えていた を動かした検証より、期待が先 していた 10 -
選択肢を KMP に絞った理由 候補 採 共有の KMP メンバーのスキルセット(Kotlin)の 延 で始められる
Swift on Android / Skip iOS → Android 当時の成熟度と私たちの運 要件では 実運 の確信を持てず対象外 全 全 置き換えが前提で、既存資産を 活かした段階導 という条件に合わず 対象外 1 用 入 方 用 長 面 ©MIXI 面 判断 Android → iOS Flutter / React Native 用 向 置き換え なぜKMPの検討を始めたか? 11 -
Swift Export Kotlin Kotlin 成する仕組み せずに済む Objective-C SKIE:Objective-C の出 vs
Swift の API が直接出てくる 込み を Swift らしくする KMP-NativeCoroutines: suspend / Flow を Swift から使いやすくする sealed class / enum は SKIE で補う Swift Export に移るなら書き直しになる りないところはライブラリ等で補う前提でSwift Export を中 見 力 生 月 2 行 ・ 1 0 なぜKMPの検討を始めたか? に検討 12 心 1 4 Swift XCFramework + SwiftPM で配れる suspend / Flow は回避策が必要だった 足 A- 安定していてすぐ使える 検討当時は Experimental ©MIXI A- + SKIE + KMP-NativeCoroutines Kotlin . . (2026 年 6 )で Alpha に suspend / Flow にも対応した あとから再移 詳しくは補 Objective-C interop Swift Kotlin から Swift を直接 2 足 検討当時の選択:Swift Export か Objective-C interop か -
UI 共有しない iOS の UI Android の UI UI は共有しない
体験の差や意図的な差があり、Liquid Glass のような OS ネイティブの 変化への追従が遅れる domain ボトムアップ model から共有する model ここから共有 api client ここから共有 プラットフォーム依存を抑えた model は( api client DB 較的)共有しやすい Key-Value store も共有する 末端の要素に依存しないモジュールなので ボトムアップに進める起点に向いている model と api client の共有を PoC で検証 Swift Export :shared Swift の API みてね iOS のコードベース ※ 検証の過程では Objective-C interop の経路も確認した 本番のコードベースでの PoC でXcode から参照でき、実際に動作することはわかった Swift Exportを中 比 見 ・ 心 ©MIXI ・ 2 1 3 2 KMPでどこまで共有するのが良いか に検討して えたこと 14 -
詳しくは補 1 data class Album( 1 public final class Album
{ 2 val id: String, 2 public var id: String { get } 3 val title: String, 3 public var title: String { get } 4 val mediaCount: Int, 4 public var mediaCount: Int32 { get } 5 ) 5 6 既存の Swift コードは struct(値型)前提 } 共有する型が増えるほど変換コストが増える Codable 前提のコード struct に詰め替える @State は編集できず差し替えるしかない 参照型のまま使う(Codable は後付けできない) copy で 部だけ変える書き (全引数が必須) → そのままだと通らない → 本来なら 度で済む定義が data class が参照型になるので iOS 側でひと Swift Exportを中 手 手 二 見 方 心 3 一 足 . . . ©MIXI 1 A- 成された公開インターフェース(抜粋) Album.kt 一 0 生 2 data class は Swift の class(参照型)となる に検討して えたこと 度 間 間かかる 15 -
Kotlin . 〜 . のころ AlbumRepository.kt AlbumListModel.swift 1 class AlbumRepository
{ 1 2 @NativeCoroutines 2 3 suspend fun loadAlbums(): List<Album> 3 4 5 @NativeCoroutines 5 6 val albums: Flow<List<Album 6 } let albums = try await asyncFunction( for: repo.loadAlbums()) for try await list in asyncSequence( for: repo.albums) { … } 7 suspend / Flow ごとに注釈を付けて回る 当時のワークアラウンド import KMPNativeCoroutinesAsync 4 7 Swift 側もライブラリのラッパー経由で呼ぶ ライブラリ経由ならキャンセルは Kotlin 側でCatch可能 当時はライブラリなしでは Flow を購読できなかった Swift Exportを中 > > 見 心 3 2 2 2 ©MIXI 2 0 2 suspend / Flow:当時は追加のワークアラウンドが必要だった に検討して えたこと 16 -
Kotlin . . AlbumRepository.kt 性モデルと噛み合わない で検証 LoadAlbumsUseCase.swift class AlbumRepository {
1 1 詳しくは補 Swift Export suspend fun loadAlbums(): List<Album> 2 let repo: AlbumRepository 3 val albums: Flow<List<Album 3 func run() async throws { … } } 4 A- A- Swift struct LoadAlbumsUseCase: Sendable { 2 4 } 5 実 される Dispatcher を Swift 側から選べない withContext は Kotlin 側に書くしかない 6 然に呼びだせる&キャンセルも Kotlin 側でCatch可能 extension AlbumRepository: @retroactive @unchecked Sendable {} 7 Sendable でない型は Sendable な型に持たせられない Kotlin から来た型 Swift (Sendable な struct) ではエラー の並 Swift Exportを中 行 > > 行 6 見 6 5 心 ・ 4 ・ 0 2 足 4 2 6 ©MIXI @unchecked Sendable 安全性の担保を iOS 側に委ねる actor に包んでも Objective-C interop でも同じ 経路を変えてもSwift 2 回避するには LoadAlbumsUseCase AlbumRepository 行 0 自 2 suspend / Flow:いまは呼べるが並 性との噛み合わなさは残る に検討して えたこと 17 -
model / api client を共有するには先にあるべき姿に置き換える作業が必要 いま使っているもの 置き換え先 joda-time kotlinx-datetime model
全般 timber KMP 対応のロガー 全体 広告 SDK まわりの型 共有の外に出す model の OkHttp + Retrofit Ktor client api client 全体 Retrofit → Ktor の書き換え 体は 影響するところ 部 さいが公開 API に依存の型を出さない整理も必要 私たちのコードベースではリファクタの実施が必須 Swift Exportを中 小 自 見 心 一 ©MIXI 3 0 2 私たちの共有対象には Android 固有の依存が多く残っていた に検討して えたこと 18 -
にも「運 モノレポ submodule の 夫」が必要 詳しくは補 A- バイナリ配布 共有コードの運 課題を
最も減らしやすい 今のリポジトリのままKMPを 始められる 2 つのリポジトリを まとめるコストが きすぎる 当時の構成ではビルド時間が 課題だった iOS 側のビルドは速い model の変更と iOS の実装を、 並 で進めにくい Objective-C interop XCFramework + SwiftPM で配れる Swift Export 公式は直接integrationのみで XCFramework は制約あり 前の コンウェイの法則:私たちはチームがドメイン単位なのに、リポジトリは iOS / Android で分かれている 共有コードの置き場もチームの単位(ドメイン)に合わせないと、運 がねじれる 現時点では共有のメリットより運 大 用 用 用 方 自 見 大 用 心 6 足 4 工 Swift Exportを中 ©MIXI 行 0 2 リポジトリ構成:どの置き 変更のコストが に検討して えたこと きい 19 -
重実装のコストは下げられる みてねの「仮想モノレポ」 ドメイン A のリポジトリ iOS すり合わせは「仮想モノレポ」で減らす ドメイン B のリポジトリ
Android iOS AI で両 OS の実装を揃える ドメインごとのリポジトリを両アプリから submodule で持ち iOS / Android の実装を 同じ場所で い差分や認識違いを減らす Android AI で両 OS の実装を揃える 「2 回書く」初期コストは AI で下がった submodule 2025 年末ごろから私たちのチームでも Kotlin から Swift への移植が実務で使える速さになった iOS アプリ Android アプリ それでも消えないコストはある 品質保証や保守のコストは残り コストは消えずに移動するだけ 同じリポジトリの中で両 OS の実装を並べて進められる 参考:Nkzn「クロスプラットフォーム開発が解決するコスト、解決しないコスト」 https://zenn.dev/nkzn/articles/cross-platform-development-costs- KMP なしでも 重実装のコストは下げられるようになった Swift Exportを中 6 2 0 2 二 見 二 心 行 ©MIXI 5 0 1 3 2 2 KMP なしでも に検討して えたこと 20 -
判断軸:メリットがデメリットを上回るまでの「距離」 前提:既存のアプリに、機能開発を続けながら段階的に入れる 距離を決めるもの 外部要因 Swift Export の成熟と Kotlin / Xcode
の 対応状況 共有構造 Android → iOS 共有の複雑さ 既存のコード 依存やリポジトリ構造の再編や リファクタリング AIの台頭 メリット − デメリット 既存システムへの導 では短期的な損得だけを ると 導 コストが先に ちやすい ▶ 損得がひっくり返るまでの距離を 積もった 損得ゼロ ひっくり返る ところ 現在地 柔軟性のなさ 時間 距離 AI で「2 回書く」コストが下がっている ※ 概念図です。定量評価ではありません 損得がひっくり返るまでの距離は今はまだ遠いと判断した 3 見 見 ・ 入 立 見 入 ©MIXI 「 送る」ことをどう意思決定したか 22 -
検証の結果 導 する 導 しない 旦 いま(2026 年 11 )
まだ 向なのも変わらず 送る」ことをどう意思決定したか 止 大 足 ・ 方 月 一 7 「 一 3 足 Android 固有の依存を剥がす 段は KMP でなくてもいい ©MIXI 手 めている間も続けること KMP 以外が出たら調べる Android → iOS 共有の複雑さ 見 だけ残る リリースノートをウォッチ 軽減 同期は解決。Sendable はまだ。SwiftPM 配布は Beta で対応 入 認知負荷の増 A- 再開は差分を調べるだけ Swift Export の機能不 ※ 再開の 詳しくは補 次に話が出たらまたゼロからの検証が必要 再開する条件 リポジトリ構成も める めたときに負債 私たちには今はまだ早い 入 二 止 非 止 択にしない:「私たちには今はまだ早い」として 23 -
れるか考えるための観点 持ち帰りチェックリスト 01 4問 技術的な成熟度 4問 コスト 途中で経路を移るときの書き直しを許容できる? 共有したいモジュールに、Android 固有の依存はいくつあ
る? で りる? 3問 チームの形 iOS と Android で、チームとリポジトリは分かれてる? 推進メンバー以外にも、メリットと運 伝わっている? 入 入 用 行 見 用 足 方 恩恵を受ける前のリファクタ費 を 積もれてる? 04 4問 再開の条件 「何が解決したら再開するか」を 1 送りを「 4 用 れる? で書ける? めている間、何をウォッチする? の負担が AI で「2 回書く」コストはどこまで下がってる? ©MIXI 既存コードに段階的に リポジトリをどう置いて、どう回す? 03 止 移 ゼロから作る? data class が参照型で公開されても、既存の設計と合う? 一 02 Swift Export と Objective-C interop、どっちでいく? 同期まわりは今の使い 行 自 非 見 止 分たちのチームで KMP を 旦停 」として残せてる? 使う Kotlin と Xcode の組み合わせでビルドが通る? まとめ:意思決定に使 した判断軸 25 -
の正解はない 現在の取り巻く環境においてゼロから作るならKMP は良い選択肢 私たちのコードベースでは段階的に れるには、まだまだギャップがある 期待 まる期待が先 していた 検証 PoC
でSwift Exportの課題や 既存の技術負債が明らかに 判断 メリットが上回る距離を測って 「現時点では」導 を 送った 質問は Ask the Speaker や懇親会でぜひ! Kodee:Kotlin のマスコット(素材:https://kotlinlang.org/docs/kotlin-brand-assets.html) 26 入 見 入 行 ©MIXI 一 高 唯 -
Appendix 検証環境:Kotlin . . / Xcode . / Swift .
. / iOS . . simulator 27 1 3 6 2 4 2 6 3 6 2 0 2 4 2 ©MIXI -
A- 成される Swift の違い Kotlin Swift Export Objective-C interop(+ SKIE)
data class final class(参照型) class(参照型) Int Int Int enum class enum になる、case 名は PHOTO のまま SKIE が enum にする、case 名は .photo sealed interface sealedType() で網羅的な switch SKIE の onEnum(of:) で網羅的な switch suspend fun async throws としてそのまま呼べる completion handler(ライブラリで async に) Flow asAsyncSequence() で購読できる ライブラリなしでは購読できない Sendable 付かない 付かない Swift 側での interface 実装 できる 補 生 1 2 2 3 3 ©MIXI 足 できない 28 -
A- Obj-C interop vs Swift Export 呼び出し側の差分 Objective-C interop +
ライブラリ 1 try await asyncFunction( 3 for: repo.albums) { … } 4 5 switch onEnum(of: state) { 5 switch state.sealedType() { 6 case .loaded(let s): s.albums.count 6 case .loaded(let s): s.value.albums.count 7 } 7 } 8 case .photo: "photo" 8 case .PHOTO: "photo" repo.albums.asAsyncSequence() { … } SharedObjC KMPNativeCoroutinesAsync → Shared .photo → .PHOTO(Kotlin の名前のまま) sealed onEnum(of:) → sealedType()、中 は .value を挟む Kotlin 側 @NativeCoroutines とラッパーのモジュールを削除 補 身 ©MIXI ・ for try await list in 4 enum の case 2 try await repo.loadAlbums() 2 for try await list in asyncSequence( import 足 1 for: repo.loadAlbums()) 2 3 Swift Export 29 -
A- data class:Swift 側で動くこと、動かないこと 回避策と、1 型あたりの追加コード Swift 側のコード 結果 a
b 動く struct に詰め替える 約 20 Set([a, b]) 動く に a.title = "x" エラー(読み取り専 Codable Sendable 定義は 重になる a.copy(title: "x") エラー(全引数が必須) JSONEncoder().encode(a) エラー(Codable でない) let n: Int = a.mediaCount エラー(Int any Sendable に入れる エラー(Sendable でない) ) ) 値型の扱いが 参照型のまま使う る、 約7 変換は不要、Sendable にはできない Codable を付ける 約6 class には後付けできず、struct 経由になる ※ フィールド 3 つの型 1 つでの例 補 入 手 用 ・ 2 3 ・ 行 二 行 行 3 = = 足 ©MIXI 30 -
A- Swift の Sendable:エラーになる範囲 試した書き 結果 Sendable な struct のプロパティに持たせる
エラー @MainActor の型(画 エラー コンパイラの検査を型ごとに Task の中で作って外に出さない Task.detached で取ってきて main に渡す エラー オブジェクトを保持する設計ができない approachable concurrency を有効にする 解決しない Objective-C interop + SKIE の経路で試す 同じエラー 手 面 6 方 放すことになる エラー 値型に変換してから渡す 型ごとに変換コードが要る 補 方 4 @unchecked Sendable を付ける actor に閉じ込めて呼ぶ ©MIXI 足 など)に持たせて呼ぶ 通る書き 31 -
A- Swift Export の 同期:呼び と実 やること Swift 側の書き 観測
suspend 関数を呼ぶ try await repo.loadAlbums() 動く Flow を購読する repo.albums.asAsyncSequence() 動く Kotlin の例外を受ける catch let e as KotlinError e.wrapped で元の例外 キャンセルする task.cancel() Kotlin 側まで届く Kotlin 側の処理が実 されるスレッド Swift 側の呼び出し元 Kotlin 側の書き 実 @MainActor の中から 何も書かない バックグラウンド Task.detached の中から 何も書かない バックグラウンド どこからでも withContext(Dispatchers.Main) メインスレッド 補 行 方 非 行 方 方 5 行 ©MIXI 足 される場所 された場所 32 -
2.2.20 2.4.0 Experimental 2.4.20 Alpha 最初のバージョン。Xcode に 直接統合して使う 2.5.0-Beta2 Alpha
suspend / Flow をそのまま呼 べるようになる Beta sealed の網羅的な switch、 Swift 側での interface 実装 Swift Package で出 できる、依 存を隠す設定が る Objective-C interop:この間ずっと XCFramework + SwiftPM で配れる 設定の書き も変わる Kotlin . .x まで Kotlin . . から swiftExport { 1 1 moduleName = "Shared" 2 3 flattenPackage = "com.example.shared" 3 moduleName = "Shared" 4 export(project(":core")) 4 rootPackage = "com.example.shared" 5 xcodeIntegration() 6 swiftPackageIntegration { … } } 力 自 入 方 0 4 5 生 2 2 6 8 補 ©MIXI swift { } 7 . .x の 成物を 前で XCFramework にすると、 Swift コンパイラの版に縛られる 4 export { 2 5 足 2 A- Swift Export のバージョンごとの変化 } 33 -
A- Kotlin . . -Beta で変わること 良くなること 変わらないこと Swift Export
が Beta になる 公開されている変更点に Sendable 対応の記載はない Swift Package として出 data class は参照型のまま できる 依存を Swift の API から隠す設定が る 実 される Dispatcher は選べないまま Apple 向けの差分コンパイルが既定で有効になる 出典 https://kotlinlang.org/docs/whatsnew-eap.html https://github.com/JetBrains/kotlin/releases/tag/v . . -Beta 補 2 0 5 2 入 2 力 0 5 2 7 行 足 ©MIXI 34