Upgrade to Pro — share decks privately, control downloads, hide ads and more …

共有と分離 - Compose Multiplatform "本番導入" の設計指針

Avatar for Ryo WATANABE Ryo WATANABE
September 11, 2025

共有と分離 - Compose Multiplatform "本番導入" の設計指針

Avatar for Ryo WATANABE

Ryo WATANABE

September 11, 2025
Tweet

More Decks by Ryo WATANABE

Other Decks in Technology

Transcript

  1. • 渡邊 亮 / naberyo (@error96num) • STORES 株式会社 •

    Android エンジニア • 最近モルックにハマっています。 2 自己紹介 2
  2. • 榎本 健太 / @enomotok_ • STORES 株式会社 • iOS

    エンジニア • 趣味はサッカー観戦と自転車で出かけることです。 3 自己紹介 3
  3. Kotlin Multiplatform (KMP) / Compose Multiplatform (CMP) 4 Shared UI

    Compose Multiplatform Shared logic Kotlin Multiplatform
  4. キッチンディスプレイ アプリに KMP / CMP を採用した 7 2024-12 Android 版

    リリース🚀 2024-08 KMP / CMP 採用 2025-03 iOS 版 リリース🚀 2024-03 プロダクト 構想
  5. 8 なぜ KMP / CMP を採用したか? 8 ①プロダクト要件との相性 • 業務アプリのため「ネイティブらしい」

    UI への期待は低い → プラットフォーム個別の実装は少なく済みそう • 先に Android、続いて iOS をリリース予定 → 仮に iOS をネイティブで作り直しになっても、 KMP / CMP のコー ドは Android の資産として活かせる(リスク半減) 2024-12 Android 版 リリース🚀 2024-08 KMP / CMP 採用 2025-03 iOS 版 リリース🚀 2024-03 プロダクト 構想
  6. 9 なぜ KMP / CMP を採用したか? 9 ②最小リソースで両 OS 対応

    • Android 3名 → Android 2名 / iOS 2名 → Android 1名 ◦ 他プロダクトの開発を兼任 • Android ネイティブの知見を活かせる 9 2024-12 Android 版 リリース🚀 2024-08 KMP / CMP 採用 2025-03 iOS 版 リリース🚀 2024-03 プロダクト 構想
  7. 10 なぜ KMP / CMP を採用したか? 10 ③当時(2024年8月)の KMP の状況

    • ✅ 安定版 • ✅ プロダクション利用実績 • ✅ KMP 対応ライブラリ 10 10 2025-03 iOS 版 リリース🚀 2024-12 Android 版 リリース🚀 2024-08 KMP / CMP 採用 2023-11 KMP 安定版✨ 2024-03 プロダクト 構想
  8. 11 なぜ KMP / CMP を採用したか? ④当時(2024年8月)の CMP の状況 •

    ⚠ iOS β版 • ⚠ プロダクション利用実績 • ✅ プロトタイピングの結果 2025-03 iOS 版 リリース🚀 2024-12 Android 版 リリース🚀 2024-05 CMP for iOS β 版✨ 2025-05 CMP for iOS 安定版✨ 2024-08 KMP / CMP 採用 2024-03 プロダクト 構想
  9. まずは要件を明確に 16 要件を明確にする Kotlin の公式共通 ライブラリで 実現できる? KMP 対応 ライブラリで

    実現できる? 共有 分離 ✅ ✅ ❌ ❌ ネットワーク/データベース 認証/ログ出力/分析/DI etc…
  10. Kotlin で共通実装できる/できない の基準 17 要件を明確にする Kotlin の公式共通 ライブラリで 実現できる? KMP

    対応 ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ Kotlin standard ライブラリ + コアライブラリ は Android / iOS で共通利用できる 例) day1 ワークショップより引用 ✅ https://docs.google.com/presentation/d/e/2PACX-1vS9Cz5ARIZ7mFuI9JyFqwZcnYKqj2kafD65w54YaU7NcwJgEy1_0fbmDK95XFt0LRT 3UHMq1L9W7JBf/pub
  11. Kotlin で共通実装できる/できない の基準 18 要件を明確にする Kotlin の公式共通 ライブラリで 実現できる? KMP

    対応 ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ プラットフォーム依存のAPIは共通利用できない 例) day1 ワークショップより引用 ❌ https://docs.google.com/presentation/d/e/2PACX-1vS9Cz5ARIZ7mFuI9JyFqwZcnYKqj2kafD65w54YaU7NcwJgEy1_0fbmDK95XFt0LRT 3UHMq1L9W7JBf/pub
  12. Kotlin で共通実装できる/できない をアプリの要件に照らし合わせる 19 要件を明確にする Kotlin の公式共通 ライブラリで 実現できる? KMP

    対応 ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ • ビジネスロジック ◦ 値の計算 ◦ 値の検証 • デバイス機能 ◦ Bluetooth ◦ GPS ◦ カメラ • 入出力 ◦ ネットワーク ◦ ファイル ◦ データベース ✅ ❌
  13. KMP 対応ライブラリの利用を検討する 20 要件を明確にする Kotlin の公式共通 ライブラリで 実現できる? KMP 対応

    ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ Klibs.io:KMP 対応ライブラリの検索サービス • GitHub と Maven Central をクロール • 対応プラットフォームでフィルタ可 • 技術カテゴリでフィルタ可 https://klibs.io/ https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-platform-specifics.html#project-structure
  14. KMP 対応ライブラリの利用を検討する 21 要件を明確にする Kotlin の公式共通 ライブラリで 実現できる? KMP 対応

    ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ Kotlin Multiplatform samples • JetBrains 公式 / コミュニティ による サンプルプロジェクト一覧 • 利用しているライブラリを掲載 https://www.jetbrains.com/help/kotlin-multiplatform-dev/multiplatform-samples.html https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-platform-specifics.html#project-structure
  15. キッチンディスプレイの技術スタック • ネットワーク ◦ REST:Ktor ◦ GraphQL:Apollo Kotlin • ローカルDB:SQLDelight

    ※2025年9月現在は Room を推奨 • 依存性注入 (DI):Koin • ログ出力:Napier 22
  16. まずは要件を明確に 24 要件を明確にする CMP 公式の スタックで 実現できる? CMP 対応 ライブラリで

    実現できる? 共有 分離 ✅ ✅ ❌ ❌ UIコンポーネント/アニメーション/ ナビゲーション/WebView etc…
  17. Compose Multiplatform 公式のスタック* での共通実装を目指す 25 要件を明確にする CMP 公式の スタックで 実現できる?

    CMP 対応 ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ CMP 公式のスタック(*本セッションでの定義) = JetBrains が提供する • Compose 基盤モジュール runtime / ui / foundation / animation • Material (M2 / M3) theme / components … • AndroidX ライブラリ (org.jetbrains.androidx.*) Lifecycle Runtime Compose / ViewModel Compose / Navigation Compose
  18. Compose Multiplatform は JetPack Compose と多くの API が共通 26 要件を明確にする

    CMP 公式の スタックで 実現できる? CMP 対応 ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌
  19. CMP 公式スタックでの共通実装が難しいケース① 27 要件を明確にする CMP 公式の スタックで 実現できる? CMP 対応

    ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ Jetpack にはあるが CMP で共通利用できないケース • シグネチャに android.* / androidx.* (※androidx.compose.*を除く)が含まれる API ◦ Maps ライブラリ / WebView クラス ... • シグネチャに android.* / androidx.* を含まないが Android だけ提供の API (CMP 1.8.2 時点) ◦ Modifier.imeNestedScroll() / material3-adaptive ... ❌ https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-android-only-components.html
  20. CMP 公式スタックでの共通実装が難しいケース② 28 要件を明確にする CMP 公式の スタックで 実現できる? CMP 対応

    ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ OS ごとに UI をあえて分けたいケース = iOS らしさを優先したい (Material の見た目が合わない) ❌
  21. CMP 対応ライブラリの利用を検討する 29 要件を明確にする CMP 公式の スタックで 実現できる? CMP 対応

    ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ Klibs.io には 50 のライブラリが掲載 (2025年9月現在)
  22. 代表的な CMP 対応のライブラリ 30 要件を明確にする CMP 公式の スタックで 実現できる? CMP

    対応 ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ compose-webview-multiplatform:  Android / iOS / デスクトップ対応 の共通 WebView ✅ https://github.com/KevinnZou/compose-webview-multiplatform
  23. 代表的な CMP 対応のライブラリ 31 要件を明確にする CMP 公式の スタックで 実現できる? CMP

    対応 ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ Calf: CMP 向けのアダプティブUI+API拡張  Adaptive UI、File Picker、Permissions etc... ✅ https://github.com/MohamedRejeb/Calf?tab=readme-ov-file
  24. 代表的な CMP 対応のライブラリ 32 要件を明確にする CMP 公式の スタックで 実現できる? CMP

    対応 ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ compose-cupertino:  iOS の Cupertino テーマとウィジェット ✅ https://github.com/alexzhirkevich/compose-cupertino
  25. キッチンディスプレイの技術スタック • ネットワーク ◦ REST:Ktor ◦ GraphQL:Apollo Kotlin • ローカルDB:SQLDelight

    ※2025年9月現在はRoomを推奨 • 依存性注入 (DI):Koin • ログ出力:Napier • ナビゲーション:Navigation Compose • 画像表示:coil • WebView:compose-webview-multiplatform • ボトムシート: FlexibleBottomSheet 37
  26. キッチンディスプレイのコード共有状況 38 Shared code iosApp androidApp androidMain commonMain iosMain shared

    • androidApp (Gradle モジュール):Android アプリのエントリーポイント • iosApp (Xcode プロジェクト):iOS アプリのエントリーポイント • shared (Gradle モジュール):KMP / CMP 対応のモジュール ◦ commonMain ソースセット:Android / iOS 共通の実装 ◦ androidMain ソースセット:Android 固有の実装 ◦ iosMain ソースセット:iOS 固有の実装
  27. expect/actual 42 Common I/F iOS-specific logic Android-specific logic iOS APIs

    Android APIs iosMain androidMain commonMain expect val … expect fun …(…) expect class … { … } actual val … actual fun …(…) actual class … { … } actual val … actual fun …(…) actual class … { … } shared
  28. ケーススタディ:通知音の再生機能 44 commonMain/AudioPlayer.kt expect class AudioPlayer { fun playSound(filePath: String)

    ./ ... } AudioPlayer.kt AudioPlayer.ios.kt AudioPlayer.android.kt AVAudioPlayer SoundPool iosMain androidMain commonMain
  29. Android の 実装 45 actual class AudioPlayer( private val context:

    Context, ) { private val soundPool = SoundPool.Builder() .setMaxStreams(1) .setAudioAttributes( AudioAttributes.Builder() .setUsage(AudioAttributes.USAGE_NOTIFICATION) .build(), ).build() @ExperimentalResourceApi actual fun playSound(filePath: String) { val uri = Res.getUri(filePath) val path = uri.removePrefix("file:--/android_asset/") val afd = context.assets.openFd(path) val soundId = soundPool.load(afd, 1) soundPool.setOnLoadCompleteListener { _, sampleId, status -> if (status -= 0 -& sampleId -= soundId) { soundPool.play(soundId, 1.0f, 1.0f, 1, 0, 1.0f) } } } ./ ... } androidMain/AudioPlayer.android.kt AudioPlayer.kt AudioPlayer.android.kt SoundPool androidMain commonMain
  30. iOS の 実装 46 actual class AudioPlayer { private var

    currentPlayer: AVAudioPlayer? = null @ExperimentalResourceApi @ExperimentalForeignApi actual fun playSound(filePath: String) { val mediaItem = NSURL.URLWithString( URLString = Res.getUri(filePath), ) mediaItem-.let { currentPlayer = AVAudioPlayer( contentsOfURL = it, error = null, ) currentPlayer-.prepareToPlay() currentPlayer-.play() } } ./ ... } iosMain/AudioPlayer.ios.kt AudioPlayer.kt AudioPlayer.ios.kt AVAudioPlayer iosMain commonMain
  31. ケーススタディ:ネイティブ UI のダイアログ表示 48 Dialog.kt Dialog.ios.kt Dialog.android.kt UIKit UIAlertController Material

    AlertDialog iosMain androidMain commonMain commonMain/Dialog.kt @Composable expect fun Dialog(state: DialogState) data class DialogState( val title: String, val text: String, val confirmButton: Button, val dismissButton: Button? = null, ) { data class Button( val text: String, val action: () -> Unit, ) }
  32. Android の実装 49 @Composable actual fun Dialog(state: DialogState) { AlertDialog(

    title = { Text(state.title) }, text = { Text(state.text) }, onDismissRequest = { state.dismissButton-.action() }, confirmButton = { state.confirmButton.let { button -> TextButton(onClick = button.action) { Text(text = button.text) } } }, dismissButton = { state.dismissButton-.let { button -> TextButton(onClick = button.action) { Text(text = button.text) } } }, ) } androidMain/Dialog.android.kt Dialog.kt Dialog.android.kt Material AlertDialog androidMain commonMain
  33. iOS の実装 50 iosMain/Dialog.ios.kt @Composable actual fun Dialog(state: DialogState) {

    val alert = UIAlertController.alertControllerWithTitle( title = state.title, message = state.text, preferredStyle = UIAlertControllerStyleAlert, ) state.confirmButton.let { button -> alert.addAction( UIAlertAction.actionWithTitle( title = button.text, style = UIAlertActionStyleDefault, handler = { button.action() }, ), ) } state.dismissButton-.let { button -> alert.addAction( UIAlertAction.actionWithTitle( title = button.text, style = UIAlertActionStyleCancel, handler = { button.action() }, ), ) } LocalUIViewController.current.showViewController( vc = alert, sender = null, ) } Dialog.kt Dialog.ios.kt UIKit UIAlertController commonMain iosMain
  34. なぜ Kotlin から iOS の API (platform.*) を import できるのか?

    51 Common I/F iOS-specific logic Android-specific logic iOS APIs Android APIs iosMain androidMain commonMain Kotlin/Native コンパイラパッケージ Apple SDK (Objective-C) cinterop ツール KMP プロジェクト import platform.AVFAudio.* import platform.UIKit.* … Objective-C型→Kotlin型マッピング iosMain/*.ios.kt プラットフォームライブラリ (.klib)
  35. そのまま import できる iOS の API 一覧 52 • Foundation(基本

    API) • UIKit(UI) • AVFoundation(オーディオ/ビデオ) • CoreBluetooth(Bluetooth) • CoreLocation(位置情報) • WebKit(WKWebView) • ... ✅ GitHub: https://github.com/JetBrains/kotlin/tree/v2.1.21/kotli n-native/platformLibs/src/platform/ios Docs(ローカル配布物の見方) : https://kotlinlang.org/docs/native-platform-libs.html #popular-native-libraries
  36. そのまま import できる iOS の API 一覧 53 • Foundation(基本

    API) • UIKit(UI) • AVFoundation(オーディオ/ビデオ) • CoreBluetooth(Bluetooth) • CoreLocation(位置情報) • WebKit(WKWebView) • ... ✅ プラットフォームライブラリとして 同梱されていない機能 • Swift-only API • 3rd party のネイティブ SDK ❌
  37. ケーススタディ:Firebase SDK の利用 Firebase SDK は KMP 非対応 • Firebase

    の KMP 対応のサードパーティーライブラリ (https://github.com/GitLiveApp/firebase-kotlin-sdk) は使いたい機 能が不足していた • 各 OS のネイティブ SDK を利用する方法を模索することに • KMP/CMP でも、ひと工夫すればネイティブのライブラリを使用可能 だと分かった 56
  38. iOS と Android のネイティブ SDK 問題を Koin を使って解決する Koin を使って

    Android, iOS それぞれのアプリモジュールから共通モ ジュールに 依存性を注入することで、ネイティブ SDK を参照する 57
  39. Android の設定はシンプル   62 dependencies { ./ ... ./ Firebase

    SDK implementation(platform(libs.firebase.bom)) implementation(libs.firebase.analytics) implementation(libs.firebase.crashlytics) implementation(libs.firebase.config) } iOSApp.swift App.kt AndroidApp.kt CrashlyticsCore.kt CrashlyticsCoreImpl.swift CrashlyticsCoreImpl.kt Firebase SDK SwiftPM Firebase SDK Gradle iosApp androidApp shared/commonMain androidApp/build.gradle.kts(抜粋)
  40. iOS で Swift のネイティブ実装を KMP から利用する方法 Swift + Koin で

    DI • commonMain に定義した interface を Swift で実装し、 Koin を用い て KMP 側にインジェクト 63 + iOSApp.swift App.kt AndroidApp.kt CrashlyticsCore.kt CrashlyticsCoreImpl.swift CrashlyticsCoreImpl.kt Firebase SDK SwiftPM Firebase SDK Gradle iosApp androidApp shared/commonMain
  41. iOS で Swift のネイティブ実装を KMP から利用する方法:補足 cinterop • Kotlin/Native がネイティブ

    SDK を呼び出すための橋渡しをする仕組 み • 公式で推奨されている方法 • 今回は使わないことに決めた 64
  42. なぜ cinterop を使わないことに決めたか 65 通常の iOS 開発とKMP/CMP + cinterop を使う場合の比較

    iOS のデファクトスタンダード cinterop を用いる方法 iOS の開発言語 Swift Swift ※ただし以下が必要 Generated Objective-C Header @objc アノテーション .def ファイル ライブラリ管理 Swift Package Manager CocoaPods
  43. なぜ cinterop を使わないことに決めたか cinterop にある Swift を使うための制約 ◦ Objective-C を経由するため、ブリッジとなるファイルが複数必要

    ◦ ライブラリのコードを参照するためにラッパーを書く必要 ◦ CocoaPods 💥 ▪ iOS 開発ではすでにレガシー ▪ SwiftPM に比べるとビルド設定が複雑 ▪ 今後 SwiftPM でしか使えないライブラリが増えるかも 66
  44. iOS で Swift のネイティブ実装を KMP から利用する方法 Swift + Koin で

    DI • commonMain に定義した interface を Swift で実装し、 Koin を用い て KMP 側にインジェクト 67 + iOSApp.swift App.kt AndroidApp.kt CrashlyticsCore.kt CrashlyticsCoreImpl.swift CrashlyticsCoreImpl.kt Firebase SDK SwiftPM Firebase SDK Gradle iosApp androidApp shared/commonMain
  45. DI の設計と実装方法 - Kotlin 側に interface を定義、Swift で実装 69 final

    class CrashlyticsCoreImpl: SharedCrashlyticsCore { private var crashlytics: Crashlytics? init() { crashlytics = Crashlytics.crashlytics() } func log(message: String) { -/ … } -/ … } iosApp/CrashlyticsCoreImpl.swift interface CrashlyticsCore { fun log(message: String) -/ --. } commonMain/CrashlyticsCore.kt iOSApp.swift App.kt AndroidApp.kt CrashlyticsCore.kt CrashlyticsCoreImpl.swift CrashlyticsCoreImpl.kt Firebase SDK SwiftPM Firebase SDK Gradle iosApp androidApp shared/commonMain
  46. DI の設計と実装方法 - Kotlin 側に interface を定義、Swift で実装 70 final

    class CrashlyticsCoreImpl: SharedCrashlyticsCore { private var crashlytics: Crashlytics? init() { crashlytics = Crashlytics.crashlytics() } func log(message: String) { -/ … } -/ … } iosApp/CrashlyticsCoreImpl.swift interface CrashlyticsCore { fun log(message: String) -/ --. } commonMain/CrashlyticsCore.kt iOSApp.swift App.kt AndroidApp.kt CrashlyticsCore.kt CrashlyticsCoreImpl.swift CrashlyticsCoreImpl.kt Firebase SDK SwiftPM Firebase SDK Gradle iosApp androidApp shared/commonMain
  47. DI の設計と実装方法 - iOS アプリのエントリーポイントで依存性を注入 71 @main struct iOSApp: SwiftUI.App

    { init() { FirebaseApp.configure() InitKoinKt.doInitKoin( config: nil, platformModule: iosNativeModule ) sharedUi.App.companion.initialize() } var body: some Scene { WindowGroup { ContentView() } } } iosApp/iOSApp.swift iOSApp.swift App.kt AndroidApp.kt CrashlyticsCore.kt CrashlyticsCoreImpl.swift CrashlyticsCoreImpl.kt Firebase SDK SwiftPM Firebase SDK Gradle iosApp androidApp shared/commonMain
  48. DI の設計と実装方法 - iOS アプリのエントリーポイントで依存性を注入 72 @main struct iOSApp: SwiftUI.App

    { init() { FirebaseApp.configure() InitKoinKt.doInitKoin( config: nil, platformModule: iosNativeModule ) sharedUi.App.companion.initialize() } var body: some Scene { WindowGroup { ContentView() } } } iosApp/iOSApp.swift iOSApp.swift App.kt AndroidApp.kt CrashlyticsCore.kt CrashlyticsCoreImpl.swift CrashlyticsCoreImpl.kt Firebase SDK SwiftPM Firebase SDK Gradle iosApp androidApp shared/commonMain
  49. DI の設計と実装方法 - iOS アプリのエントリーポイントで依存性を注入 73 @main struct iOSApp: SwiftUI.App

    { init() { FirebaseApp.configure() InitKoinKt.doInitKoin( config: nil, platformModule: iosNativeModule ) sharedUi.App.companion.initialize() } var body: some Scene { WindowGroup { ContentView() } } } var iosNativeModule: Koin_coreModule = MakeNativeModuleKt.makeNativeModule( krashlyticsCore: { _ in KrashlyticsCoreImpl() }, remoteConfigCore: { _ in RemoteConfigCoreImpl() } ) iosApp/iOSApp.swift
  50. DI の設計と実装方法 - iOS アプリのエントリーポイントで依存性を注入 74 @main struct iOSApp: SwiftUI.App

    { init() { FirebaseApp.configure() InitKoinKt.doInitKoin( config: nil, platformModule: iosNativeModule ) sharedUi.App.companion.initialize() } var body: some Scene { WindowGroup { ContentView() } } } var iosNativeModule: Koin_coreModule = MakeNativeModuleKt.makeNativeModule( krashlyticsCore: { _ in KrashlyticsCoreImpl() }, remoteConfigCore: { _ in RemoteConfigCoreImpl() } ) iosApp/iOSApp.swift typealias NativeInjectionFactory<T> = Scope.() -> T fun makeNativeModule( krashlyticsCore: NativeInjectionFactory<KrashlyticsCore>, remoteConfigCore: NativeInjectionFactory<RemoteConfigCore>, ): Module = module { single { krashlyticsCore() } single { remoteConfigCore() } } iosMain/makeNativeModule.kt
  51. DI の設計と実装方法 - InitKoin で依存性を注入 75 commonMain/InitKoin.kt fun initKoin( config:

    KoinAppDeclaration? = null, platformModule: Module, ) { val modules = listOf( ./ 各OS専用のKoinモジュール platformModule, ./ Kotlinで実装された共通のKoinモジュール commonSharedUiModule, commonCoreDomainModule, ./ ... ) KoinDispatcher.start( modules = modules, config = config, ) } iOSApp.swift App.kt AndroidApp.kt CrashlyticsCore.kt CrashlyticsCoreImpl.swift CrashlyticsCoreImpl.kt Firebase SDK SwiftPM Firebase SDK Gradle iosApp androidApp shared/commonMain InitKoin.kt
  52. 今後の展望 将来的には、もっとシームレスに KMP/CMP から Swift のコードを参照でき るようになるかもしれない? • 😢 Swift

    Import は JetBrains のロードマップには載っていない ◦ Kotlin から Swift を参照するためのしくみ ◦ イシュートラッカーにいくつか要望が起票されている状況 • ☺ Swift Export ◦ Swift から Kotlin を参照するためのしくみ(今回とは逆方向) ◦ Kotlin 2.2.20 で experimental に 76
  53. ふりかえり KMP / CMP を採用した背景 • プラットフォーム固有の実装を減らすと同時に、ネイティブで作り直すリスクに備えた ◦ 結果 ▪

    ☀ 91.5% のコードを共通化できた( 複雑なロジック・UIも!) ▪ ☀ iOS のほとんどのコードを共通実装でまかなうことができた • 少人数のチーム事情 ◦ 結果 ▪ ☀ 予定通りリリースできた ▪ ☀ チームの人数が減った現在も活発に機能追加をしている 78
  54. ふりかえり KMP / CMP を本番導入して見えてきた課題も • ⛅ Android で使えるライブラリの制限 •

    ☁ iOS のデバッグが難しいことがある • ☁ ビルド時間 この話は次の機会に 80
  55. 共有と分離 まとめ 共有できない領域を事前に見極め、出てきたら迷わず分離する 1. コード共有の限界を見極める • 素早く判断 • UI 以外:Kotlin 公式

    → KMP 対応 Lib → それでも無理なら分離 • UI:CMP 公式 → CMP 対応 Lib → それでも無理なら分離 • リスク箇所は小さくプロトタイプ 2. コードを分離する • expect / actual で境界を定義(UI 以外 / UI どちらも) • ネイティブ SDK の利用:Kotlin ⇆ Swift を DI で橋渡し 81