Slide 1

Slide 1 text

2025-09-11 DroidKaigi 2025 naberyo / Kenta Enomoto 共有と分離 Compose Multiplatform “本番導入” の設計指針

Slide 2

Slide 2 text

● 渡邊 亮 / naberyo (@error96num) ● STORES 株式会社 ● Android エンジニア ● 最近モルックにハマっています。 2 自己紹介 2

Slide 3

Slide 3 text

● 榎本 健太 / @enomotok_ ● STORES 株式会社 ● iOS エンジニア ● 趣味はサッカー観戦と自転車で出かけることです。 3 自己紹介 3

Slide 4

Slide 4 text

Kotlin Multiplatform (KMP) / Compose Multiplatform (CMP) 4 Shared UI Compose Multiplatform Shared logic Kotlin Multiplatform

Slide 5

Slide 5 text

5 共有と分離 何を話す? 「共有できない領域」を事前に見極め、出てきたら迷わず分離する 設計と実装のやり方 なぜ話す? 開発に必要な体制・コスト・スケジュールを予測可能にするため 誰に向けて話す? これから KMP / CMP の導入を検討している方 導入中で「共有できない領域」に直面している方

Slide 6

Slide 6 text

6 STORES モバイルオーダー・キッチンディスプレイ 6

Slide 7

Slide 7 text

キッチンディスプレイ アプリに KMP / CMP を採用した 7 2024-12 Android 版 リリース🚀 2024-08 KMP / CMP 採用 2025-03 iOS 版 リリース🚀 2024-03 プロダクト 構想

Slide 8

Slide 8 text

8 なぜ KMP / CMP を採用したか? 8 ①プロダクト要件との相性 ● 業務アプリのため「ネイティブらしい」 UI への期待は低い → プラットフォーム個別の実装は少なく済みそう ● 先に Android、続いて iOS をリリース予定 → 仮に iOS をネイティブで作り直しになっても、 KMP / CMP のコー ドは Android の資産として活かせる(リスク半減) 2024-12 Android 版 リリース🚀 2024-08 KMP / CMP 採用 2025-03 iOS 版 リリース🚀 2024-03 プロダクト 構想

Slide 9

Slide 9 text

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 プロダクト 構想

Slide 10

Slide 10 text

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 プロダクト 構想

Slide 11

Slide 11 text

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 プロダクト 構想

Slide 12

Slide 12 text

12 😢 採用当時 課題に感じていたこと 開発に必要な体制・コスト・スケジュールを見積もるのが難しい ● どこまで共通実装が可能で、どこでプラットフォーム固有の実装が 必要になるのか わからなかったため ● プラットフォーム固有の実装が必要になったとき、適切にコードを 分離する方法が わからなかったため

Slide 13

Slide 13 text

13 アジェンダ 13 ● コード共有の限界を見極める ● コード分離 基礎編:expect / actual 活用 ● コード分離 応用編:ネイティブ SDK の利用

Slide 14

Slide 14 text

14 アジェンダ 14 ● コード共有の限界を見極める ● コード分離 基礎編:expect / actual 活用 ● コード分離 応用編:ネイティブ SDK の利用

Slide 15

Slide 15 text

UI 以外のコード共通化戦略 15 Shared UI Compose Multiplatform Shared logic Kotlin Multiplatform

Slide 16

Slide 16 text

まずは要件を明確に 16 要件を明確にする Kotlin の公式共通 ライブラリで 実現できる? KMP 対応 ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ ネットワーク/データベース 認証/ログ出力/分析/DI etc…

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

Kotlin で共通実装できる/できない をアプリの要件に照らし合わせる 19 要件を明確にする Kotlin の公式共通 ライブラリで 実現できる? KMP 対応 ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ ● ビジネスロジック ○ 値の計算 ○ 値の検証 ● デバイス機能 ○ Bluetooth ○ GPS ○ カメラ ● 入出力 ○ ネットワーク ○ ファイル ○ データベース ✅ ❌

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

キッチンディスプレイの技術スタック ● ネットワーク ○ REST:Ktor ○ GraphQL:Apollo Kotlin ● ローカルDB:SQLDelight ※2025年9月現在は Room を推奨 ● 依存性注入 (DI):Koin ● ログ出力:Napier 22

Slide 23

Slide 23 text

UI のコード共通化戦略 23 Shared UI Compose Multiplatform Shared logic Kotlin Multiplatform

Slide 24

Slide 24 text

まずは要件を明確に 24 要件を明確にする CMP 公式の スタックで 実現できる? CMP 対応 ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ UIコンポーネント/アニメーション/ ナビゲーション/WebView etc…

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

Compose Multiplatform は JetPack Compose と多くの API が共通 26 要件を明確にする CMP 公式の スタックで 実現できる? CMP 対応 ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

代表的な CMP 対応のライブラリ 32 要件を明確にする CMP 公式の スタックで 実現できる? CMP 対応 ライブラリで 実現できる? 共有 分離 ✅ ✅ ❌ ❌ compose-cupertino:  iOS の Cupertino テーマとウィジェット ✅ https://github.com/alexzhirkevich/compose-cupertino

Slide 33

Slide 33 text

キッチンディスプレイの技術スタック - ナビゲーション:Navigation Compose ※2025年9月現在の最新は RC 版 CMP 1.8.0+ で iOS の 戻るジェスチャーをサポート 33

Slide 34

Slide 34 text

キッチンディスプレイの技術スタック - 画像表示:coil 3.0.0+ で CMP 対応 34

Slide 35

Slide 35 text

35 キッチンディスプレイの技術スタック - WebView:compose-webview-multiplatform

Slide 36

Slide 36 text

キッチンディスプレイの技術スタック - ボトムシート: FlexibleBottomSheet 36

Slide 37

Slide 37 text

キッチンディスプレイの技術スタック ● ネットワーク ○ REST:Ktor ○ GraphQL:Apollo Kotlin ● ローカルDB:SQLDelight ※2025年9月現在はRoomを推奨 ● 依存性注入 (DI):Koin ● ログ出力:Napier ● ナビゲーション:Navigation Compose ● 画像表示:coil ● WebView:compose-webview-multiplatform ● ボトムシート: FlexibleBottomSheet 37

Slide 38

Slide 38 text

キッチンディスプレイのコード共有状況 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 固有の実装

Slide 39

Slide 39 text

キッチンディスプレイのコード共有状況 39 Shared code iosApp androidApp androidMain commonMain iosMain shared

Slide 40

Slide 40 text

キッチンディスプレイのコード共有状況 40 次章では、プラットフォーム間で 共有できなかった 8.5% のコードについて解説 Shared code

Slide 41

Slide 41 text

41 アジェンダ 41 ● コード共有の限界を見極める ● コード分離 基礎編:expect / actual 活用 ● コード分離 応用編:ネイティブ SDK の利用

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

UI 以外での expect/actual 活用 43 Shared UI Compose Multiplatform Shared logic Kotlin Multiplatform

Slide 44

Slide 44 text

ケーススタディ:通知音の再生機能 44 commonMain/AudioPlayer.kt expect class AudioPlayer { fun playSound(filePath: String) ./ ... } AudioPlayer.kt AudioPlayer.ios.kt AudioPlayer.android.kt AVAudioPlayer SoundPool iosMain androidMain commonMain

Slide 45

Slide 45 text

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

Slide 46

Slide 46 text

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

Slide 47

Slide 47 text

UI での expect/actual 活用 47 Shared UI Compose Multiplatform Shared logic Kotlin Multiplatform

Slide 48

Slide 48 text

ケーススタディ:ネイティブ 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, ) }

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

なぜ 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)

Slide 52

Slide 52 text

そのまま 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

Slide 53

Slide 53 text

そのまま import できる iOS の API 一覧 53 ● Foundation(基本 API) ● UIKit(UI) ● AVFoundation(オーディオ/ビデオ) ● CoreBluetooth(Bluetooth) ● CoreLocation(位置情報) ● WebKit(WKWebView) ● ... ✅ プラットフォームライブラリとして 同梱されていない機能 ● Swift-only API ● 3rd party のネイティブ SDK ❌

Slide 54

Slide 54 text

54 アジェンダ 54 ● コード共有の限界を見極める ● コード分離 基礎編:expect/actual 活用 ● コード分離 応用編:ネイティブ SDK の利用

Slide 55

Slide 55 text

ネイティブ SDK の利用 想定しているケース ● KMP 非対応のネイティブ実装の SDK を KMP/CMP アプリから使う 55

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

iOS と Android のネイティブ SDK 問題を Koin を使って解決する Koin を使って Android, iOS それぞれのアプリモジュールから共通モ ジュールに 依存性を注入することで、ネイティブ SDK を参照する 57

Slide 58

Slide 58 text

ソースセットを用いたコード分離 58 androidMain commonMain iosMain expect/actual はソースセットを用いたコード分離

Slide 59

Slide 59 text

モジュールを用いたコード分離 59 iosApp androidApp iosApp.swift androidMain commonMain androidApp.kt iosMain shared

Slide 60

Slide 60 text

モジュールを用いたコード分離 60 iosApp androidApp iosApp.swift App.kt CrashlyticsCore.kt CrashlyticsCoreImpl.swift CrashlyticsCoreImpl.kt Firebase SDK SwiftPM Firebase SDK Gradle commonMain androidApp.kt shared

Slide 61

Slide 61 text

commonMain モジュールを用いたコード分離 61 iosApp androidApp iosApp.swift App.kt CrashlyticsCore.kt CrashlyticsCoreImpl.swift CrashlyticsCoreImpl.kt Firebase SDK SwiftPM Firebase SDK Gradle androidApp.kt shared

Slide 62

Slide 62 text

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(抜粋)

Slide 63

Slide 63 text

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

Slide 64

Slide 64 text

iOS で Swift のネイティブ実装を KMP から利用する方法:補足 cinterop ● Kotlin/Native がネイティブ SDK を呼び出すための橋渡しをする仕組 み ● 公式で推奨されている方法 ● 今回は使わないことに決めた 64

Slide 65

Slide 65 text

なぜ cinterop を使わないことに決めたか 65 通常の iOS 開発とKMP/CMP + cinterop を使う場合の比較 iOS のデファクトスタンダード cinterop を用いる方法 iOS の開発言語 Swift Swift ※ただし以下が必要 Generated Objective-C Header @objc アノテーション .def ファイル ライブラリ管理 Swift Package Manager CocoaPods

Slide 66

Slide 66 text

なぜ cinterop を使わないことに決めたか cinterop にある Swift を使うための制約 ○ Objective-C を経由するため、ブリッジとなるファイルが複数必要 ○ ライブラリのコードを参照するためにラッパーを書く必要 ○ CocoaPods 💥 ■ iOS 開発ではすでにレガシー ■ SwiftPM に比べるとビルド設定が複雑 ■ 今後 SwiftPM でしか使えないライブラリが増えるかも 66

Slide 67

Slide 67 text

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

Slide 68

Slide 68 text

Swift のネイティブ実装を KMP から利用する方法 Xcode でパッケージ依存を追加 68

Slide 69

Slide 69 text

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

Slide 70

Slide 70 text

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

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

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 = Scope.() -> T fun makeNativeModule( krashlyticsCore: NativeInjectionFactory, remoteConfigCore: NativeInjectionFactory, ): Module = module { single { krashlyticsCore() } single { remoteConfigCore() } } iosMain/makeNativeModule.kt

Slide 75

Slide 75 text

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

Slide 76

Slide 76 text

今後の展望 将来的には、もっとシームレスに KMP/CMP から Swift のコードを参照でき るようになるかもしれない? ● 😢 Swift Import は JetBrains のロードマップには載っていない ○ Kotlin から Swift を参照するためのしくみ ○ イシュートラッカーにいくつか要望が起票されている状況 ● ☺ Swift Export ○ Swift から Kotlin を参照するためのしくみ(今回とは逆方向) ○ Kotlin 2.2.20 で experimental に 76

Slide 77

Slide 77 text

まとめ 77

Slide 78

Slide 78 text

ふりかえり KMP / CMP を採用した背景 ● プラットフォーム固有の実装を減らすと同時に、ネイティブで作り直すリスクに備えた ○ 結果 ■ ☀ 91.5% のコードを共通化できた( 複雑なロジック・UIも!) ■ ☀ iOS のほとんどのコードを共通実装でまかなうことができた ● 少人数のチーム事情 ○ 結果 ■ ☀ 予定通りリリースできた ■ ☀ チームの人数が減った現在も活発に機能追加をしている 78

Slide 79

Slide 79 text

ふりかえり KMP / CMP を本番導入して見えてきた課題も ● ⛅ Android で使えるライブラリの制限 ● ☁ iOS のデバッグが難しいことがある ● ☁ ビルド時間 79

Slide 80

Slide 80 text

ふりかえり KMP / CMP を本番導入して見えてきた課題も ● ⛅ Android で使えるライブラリの制限 ● ☁ iOS のデバッグが難しいことがある ● ☁ ビルド時間 この話は次の機会に 80

Slide 81

Slide 81 text

共有と分離 まとめ 共有できない領域を事前に見極め、出てきたら迷わず分離する 1. コード共有の限界を見極める ● 素早く判断 ● UI 以外:Kotlin 公式 → KMP 対応 Lib → それでも無理なら分離 ● UI:CMP 公式 → CMP 対応 Lib → それでも無理なら分離 ● リスク箇所は小さくプロトタイプ 2. コードを分離する ● expect / actual で境界を定義(UI 以外 / UI どちらも) ● ネイティブ SDK の利用:Kotlin ⇆ Swift を DI で橋渡し 81

Slide 82

Slide 82 text

メッセージ Kotlin Mutiplatform / Compose Multiplatform の引き出しを 一緒に増やしていきましょう! 今日の話も、その一助となれば幸いです。 82

Slide 83

Slide 83 text

モバイルオーダー・キッチンディスプレイを体験!               Android / iOS 両方あります!