iOSとAndroidでより多くのロジックを共有する
このチュートリアルではIntelliJ IDEAを使用していますが、Android Studioでも同じように進めることができます。どちらのIDEも同じコア機能とKotlin Multiplatformサポートを共有しています。
これは、「共有ロジックとネイティブUIを備えたKotlin Multiplatformアプリを作成する」チュートリアルの第4部です。先に進む前に、前のステップを完了していることを確認してください。
外部依存関係を使用して共通ロジックを実装したので、より複雑なロジックを追加し始めることができます。ネットワークリクエストとデータシリアライズは、Kotlin Multiplatformを使用してコードを共有する最も一般的なユースケースです。このオンボーディングジャーニーを完了した後に将来のプロジェクトでそれらを使用できるように、最初のアプリケーションでそれらを実装する方法を学びましょう。
更新されたアプリは、SpaceX APIからインターネット経由でデータを取得し、SpaceXロケットの最後の成功した打ち上げ日を表示します。
プロジェクトの最終状態は、異なるコルーチンソリューションを持つGitHubリポジトリの2つのブランチで確認できます。
依存関係を追加する
プロジェクトに以下のマルチプラットフォームライブラリを追加する必要があります。
kotlinx.coroutines:同時操作を可能にする非同期コードにコルーチンを使用するため。kotlinx.serialization:JSONレスポンスを、ネットワーク操作の処理に使用されるエンティティクラスのオブジェクトにデシリアライズするため。- Ktor:インターネット経由でデータを取得するためのHTTPクライアントを作成するためのフレームワーク。
kotlinx.coroutines
kotlinx.coroutinesをプロジェクトに追加するには、共通ソースセットで依存関係を指定します。これを行うには、shared/build.gradle.ktsファイルに次の行を追加します。
kotlin {
// ...
sourceSets {
commonMain.dependencies {
// ...
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2")
}
}
}Multiplatform Gradleプラグインは、kotlinx.coroutinesのプラットフォーム固有(iOSおよびAndroid)の部分に自動的に依存関係を追加します。
kotlinx.serialization
kotlinx.serializationライブラリを使用するには、対応するGradleプラグインを設定します。 これを行うには、shared/build.gradle.ktsファイルの冒頭にある既存のplugins {}ブロックに次の行を追加します。
plugins {
// ...
kotlin("plugin.serialization") version "2.2.21"
}Ktor
共有モジュールの共通ソースセットにコア依存関係(ktor-client-core)を追加する必要があります。 さらに、サポートする依存関係も追加する必要があります。
- 特定の形式でコンテンツをシリアライズおよびデシリアライズできる
ContentNegotiation機能(ktor-client-content-negotiation)を追加します。 - KtorにJSON形式と
kotlinx.serializationをシリアライズライブラリとして使用するように指示するために、ktor-serialization-kotlinx-json依存関係を追加します。KtorはJSONデータを期待し、応答を受信したときにそれをデータクラスにデシリアライズします。 - プラットフォームソースセット(
ktor-client-android、ktor-client-darwin)の対応するアーティファクトに依存関係を追加することで、プラットフォームエンジンを提供します。
kotlin {
// ...
val ktorVersion = "3.3.1"
sourceSets {
commonMain.dependencies {
// ...
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
}
androidMain.dependencies {
implementation("io.ktor:ktor-client-android:$ktorVersion")
}
iosMain.dependencies {
implementation("io.ktor:ktor-client-darwin:$ktorVersion")
}
}
}Sync Gradle Changesボタンをクリックして、Gradleファイルを同期します。
APIリクエストを作成する
データを取得するためにSpaceX APIを使用し、v4/launchesエンドポイントからすべての打ち上げのリストを取得するための単一のメソッドを使用します。
データモデルを追加する
shared/src/commonMain/.../greetingkmpディレクトリに新しいRocketLaunch.ktファイルを作成し、SpaceX APIからデータを格納するデータクラスを追加します。
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class RocketLaunch (
@SerialName("flight_number")
val flightNumber: Int,
@SerialName("name")
val missionName: String,
@SerialName("date_utc")
val launchDateUTC: String,
@SerialName("success")
val launchSuccess: Boolean?,
)RocketLaunchクラスには@Serializableアノテーションが付けられているため、kotlinx.serializationプラグインは自動的にデフォルトのシリアライザーを生成できます。@SerialNameアノテーションを使用すると、フィールド名を再定義できるため、データクラスでプロパティをより読みやすい名前で宣言できます。
HTTPクライアントを接続する
shared/src/commonMain/.../greetingkmpディレクトリに新しいRocketComponentクラスを作成します。HTTP GETリクエストを通じてロケット打ち上げ情報を取得するための
httpClientプロパティを追加します。kotlinimport io.ktor.client.HttpClient import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json class RocketComponent { private val httpClient = HttpClient { install(ContentNegotiation) { json(Json { prettyPrint = true isLenient = true ignoreUnknownKeys = true }) } } }- ContentNegotiation KtorプラグインとJSONシリアライザーは、GETリクエストの結果をデシリアライズします。
- ここでのJSONシリアライザーは、
prettyPrintプロパティによりJSONをより読みやすい形式で出力するように設定されています。isLenientにより不正な形式のJSONを読み取る際に柔軟性が高まり、ignoreUnknownKeysによりロケット打ち上げモデルで宣言されていないキーを無視します。
RocketComponentにgetDateOfLastSuccessfulLaunch()サスペンド関数を追加します。kotlinclass RocketComponent { // ... private suspend fun getDateOfLastSuccessfulLaunch(): String { } }httpClient.get()関数を呼び出して、ロケット打ち上げ情報を取得します。kotlinimport io.ktor.client.request.get import io.ktor.client.call.body class RocketComponent { // ... private suspend fun getDateOfLastSuccessfulLaunch(): String { val rockets: List<RocketLaunch> = httpClient.get("https://api.spacexdata.com/v4/launches").body() } }httpClient.get()もサスペンド関数です。これは、スレッドをブロックせずにネットワーク経由で非同期にデータを取得する必要があるためです。- サスペンド関数は、コルーチンまたは他のサスペンド関数からのみ呼び出すことができます。これが
getDateOfLastSuccessfulLaunch()がsuspendキーワードでマークされた理由です。ネットワークリクエストはHTTPクライアントのスレッドプールで実行されます。
関数を再度更新して、リスト内の最後の成功した打ち上げを見つけます。
kotlinclass RocketComponent { // ... private suspend fun getDateOfLastSuccessfulLaunch(): String { val rockets: List<RocketLaunch> = httpClient.get("https://api.spacexdata.com/v4/launches").body() val lastSuccessLaunch = rockets.last { it.launchSuccess == true } } }ロケット打ち上げのリストは、古いものから新しいものへと日付順にソートされています。
打ち上げ日をUTCからローカル日時に変換し、出力をフォーマットします。
kotlinimport kotlinx.datetime.TimeZone import kotlinx.datetime.toLocalDateTime import kotlin.time.ExperimentalTime import kotlin.time.Instant class RocketComponent { // ... @OptIn(ExperimentalTime::class) private suspend fun getDateOfLastSuccessfulLaunch(): String { val rockets: List<RocketLaunch> = httpClient.get("https://api.spacexdata.com/v4/launches").body() val lastSuccessLaunch = rockets.last { it.launchSuccess == true } val date = Instant.parse(lastSuccessLaunch.launchDateUTC) .toLocalDateTime(TimeZone.currentSystemDefault()) return "${date.month} ${date.day}, ${date.year}" } }日付は「MMMM DD, YYYY」形式になります(例:OCTOBER 5, 2022)。
getDateOfLastSuccessfulLaunch()関数を使用してメッセージを作成する、もう1つのサスペンド関数launchPhrase()を追加します。kotlinclass RocketComponent { // ... suspend fun launchPhrase(): String = try { "The last successful launch was on ${getDateOfLastSuccessfulLaunch()} 🚀" } catch (e: Exception) { println("Exception during getting the date of the last successful launch $e") "Error occurred" } }
Flowを作成する
サスペンド関数の代わりにFlowを使用できます。これらは、サスペンド関数が返す単一の値ではなく、値のシーケンスを発行します。
shared/src/commonMain/kotlinディレクトリにあるGreeting.ktファイルを開きます。GreetingクラスにrocketComponentプロパティを追加します。このプロパティには、最後の成功した打ち上げ日を含むメッセージが格納されます。kotlinprivate val rocketComponent = RocketComponent()greet()関数がFlowを返すように変更します。kotlinimport kotlinx.coroutines.delay import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.flow import kotlin.time.Duration.Companion.seconds class Greeting { // ... fun greet(): Flow<String> = flow { emit(if (Random.nextBoolean()) "Hi!" else "Hello!") delay(1.seconds) emit("Guess what this is! > ${platform.name.reversed()}") delay(1.seconds) emit(daysPhrase()) emit(rocketComponent.launchPhrase()) } }Flowは、すべてのステートメントをラップするflow()ビルダー関数でここに作成されます。Flowは、各発行間に1秒の遅延を伴って文字列を発行します。最後の要素は、ネットワーク応答が返された後にのみ発行されるため、正確な遅延はネットワークによって異なります。
インターネットアクセス権限を追加する
インターネットにアクセスするには、Androidアプリケーションに適切な権限が必要です。すべてのネットワークリクエストは共有モジュールから行われるため、そのマニフェストにインターネットアクセス権限を追加するのが理にかなっています。
composeApp/src/androidMain/AndroidManifest.xmlファイルをアクセス権限で更新します。
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
...
</manifest>greet()関数の戻り値の型をFlowに変更することで、共有モジュールのAPIはすでに更新されています。 次に、greet()関数呼び出しの結果を適切に処理できるように、プロジェクトのネイティブ部分を更新する必要があります。
ネイティブAndroid UIを更新する
共有モジュールとAndroidアプリケーションの両方がKotlinで記述されているため、Androidから共有コードを使用するのは簡単です。
ビューモデルを導入する
アプリケーションがより複雑になるにつれて、UIを実装するApp()関数を呼び出すAndroidアクティビティであるMainActivityにビューモデルを導入する時が来ました。 ビューモデルはアクティビティからのデータを管理し、アクティビティがライフサイクル変更を受けても消滅しません。
composeApp/src/androidMain/.../greetingkmpディレクトリに、新しいMainViewModelKotlinクラスを作成します。kotlinimport androidx.lifecycle.ViewModel class MainViewModel : ViewModel() { // ... }このクラスはAndroidの
ViewModelクラスを拡張しており、ライフサイクルと設定変更に関して正しい動作を保証します。StateFlow型の
greetingList値と、そのバッキングプロパティを作成します。kotlinimport kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow class MainViewModel : ViewModel() { private val _greetingList = MutableStateFlow<List<String>>(listOf()) val greetingList: StateFlow<List<String>> get() = _greetingList }- ここでの
StateFlowはFlowインターフェースを拡張していますが、単一の値または状態を持ちます。 - プライベートなバッキングプロパティ
_greetingListは、このクラスのクライアントのみが読み取り専用のgreetingListプロパティにアクセスできることを保証します。
- ここでの
View Modelの
init関数で、Greeting().greet()フローからすべての文字列を収集します。kotlinimport androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch class MainViewModel : ViewModel() { private val _greetingList = MutableStateFlow<List<String>>(listOf()) val greetingList: StateFlow<List<String>> get() = _greetingList init { viewModelScope.launch { Greeting().greet().collect { phrase -> //... } } } }collect()関数はサスペンドされるため、ビューモデルのスコープ内でlaunchコルーチンが使用されます。 これは、launchコルーチンがビューモデルのライフサイクルの正しいフェーズ中のみ実行されることを意味します。collectの後続ラムダ内で、収集されたphraseをlist内のフレーズのリストに追加するように_greetingListの値を更新します。kotlinimport kotlinx.coroutines.flow.update class MainViewModel : ViewModel() { //... init { viewModelScope.launch { Greeting().greet().collect { phrase -> _greetingList.update { list -> list + phrase } } } } }update()関数は値を自動的に更新します。
ビューモデルのFlowを使用する
composeApp/src/androidMain/kotlinにあるApp.ktファイルを開き、以前の実装を置き換えるように更新します。kotlinimport androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.compose.runtime.getValue import androidx.lifecycle.viewmodel.compose.viewModel @Composable @Preview fun App(mainViewModel: MainViewModel = viewModel()) { MaterialTheme { val greetings by mainViewModel.greetingList.collectAsStateWithLifecycle() Column( modifier = Modifier .safeContentPadding() .fillMaxSize(), verticalArrangement = Arrangement.spacedBy(8.dp), ) { greetings.forEach { greeting -> Text(greeting) HorizontalDivider() } } } }greetingListに対するcollectAsStateWithLifecycle()関数呼び出しは、ViewModelのFlowから値を収集し、ライフサイクルを意識した方法でそれをコンポーザブルステートとして表現します。- 新しいFlowが作成されると、コンポーズの状態が変更され、区切り線で区切られたグリーティングフレーズが垂直に配置されたスクロール可能な
Columnが表示されます。
結果を確認するには、composeApp構成を再実行します。

ネイティブiOS UIを更新する
プロジェクトのiOS部分では、ビジネスロジックをすべて含む共有モジュールにUIを接続するために、Model–view–viewmodelパターンを再び利用します。
モジュールはContentView.swiftファイルにimport Shared宣言で既にインポートされています。
ViewModelを導入する
iosApp/ContentView.swiftで、ContentViewのViewModelクラスを作成し、それのためのデータを準備および管理します。 並行処理をサポートするために、startObserving()関数をtask()呼び出し内で呼び出します。
import SwiftUI
import Shared
struct ContentView: View {
@ObservedObject private(set) var viewModel: ViewModel
var body: some View {
ListView(phrases: viewModel.greetings)
.task { await self.viewModel.startObserving() }
}
}
extension ContentView {
@MainActor
class ViewModel: ObservableObject {
@Published var greetings: Array<String> = []
func startObserving() {
// ...
}
}
}
struct ListView: View {
let phrases: Array<String>
var body: some View {
List(phrases, id: \.self) {
Text($0)
}
}
}ViewModelはContentViewの拡張として宣言されており、密接に関連しています。ViewModelには、Stringフレーズの配列であるgreetingsプロパティがあります。 SwiftUIはViewModel(ContentView.ViewModel)をビュー(ContentView)に接続します。ContentView.ViewModelはObservableObjectとして宣言されています。@Publishedラッパーはgreetingsプロパティに使用されます。@ObservedObjectプロパティラッパーはViewModelを購読するために使用されます。
このViewModelは、このプロパティが変更されるたびにシグナルを発行します。 次に、Flowを消費するためにstartObserving()関数を実装する必要があります。
iOSからFlowを消費するためのライブラリを選択する
このチュートリアルでは、iOSでFlowを操作するのに役立つSKIEまたはKMP-NativeCoroutinesライブラリを使用できます。 どちらもオープンソースソリューションであり、Kotlin/Nativeコンパイラがまだデフォルトで提供していないFlowによるキャンセルとジェネリクスをサポートしています。
- SKIEライブラリは、Kotlinコンパイラによって生成されたObjective-C APIを拡張します。SKIEはFlowをSwiftの
AsyncSequenceと同等のものに変換します。SKIEは、スレッド制限なしで、自動的な双方向キャンセルを伴うSwiftのasync/awaitを直接サポートします(CombineとRxSwiftにはアダプターが必要です)。SKIEは、さまざまなKotlin型をSwiftの同等型にブリッジすることを含め、KotlinからSwiftフレンドリーなAPIを生成するための他の機能も提供します。また、iOSプロジェクトに追加の依存関係を追加する必要もありません。 - KMP-NativeCoroutinesライブラリは、必要なラッパーを生成することで、iOSからサスペンド関数とFlowを消費するのに役立ちます。 KMP-NativeCoroutinesは、Swiftの
async/await機能、Combine、RxSwiftをサポートしています。 KMP-NativeCoroutinesを使用するには、iOSプロジェクトにSPMまたはCocoaPodの依存関係を追加する必要があります。
オプション1. KMP-NativeCoroutinesを構成する
ライブラリの最新バージョンを使用することをお勧めします。 プラグインの新しいバージョンが利用可能かどうかは、KMP-NativeCoroutinesリポジトリで確認してください。
プロジェクトのルート
build.gradle.ktsファイル(shared/build.gradle.ktsファイルではない)のplugins {}ブロックにKSP (Kotlin Symbol Processor)とKMP-NativeCoroutinesプラグインを追加します。kotlinplugins { // ... id("com.google.devtools.ksp").version("2.2.10-2.0.2").apply(false) id("com.rickclephas.kmp.nativecoroutines").version("1.0.0-ALPHA-45").apply(false) }shared/build.gradle.ktsファイルにKMP-NativeCoroutinesプラグインを追加します。kotlinplugins { // ... id("com.google.devtools.ksp") id("com.rickclephas.kmp.nativecoroutines") }同じく
shared/build.gradle.ktsファイルで、実験的な@ObjCNameアノテーションをオプトインします。kotlinkotlin { // ... sourceSets{ all { languageSettings { optIn("kotlin.experimental.ExperimentalObjCName") optIn("kotlin.time.ExperimentalTime") } } // ... } }Sync Gradle Changesボタンをクリックして、Gradleファイルを同期します。
KMP-NativeCoroutinesでFlowをマークする
shared/src/commonMain/kotlinディレクトリのGreeting.ktファイルを開きます。greet()関数に@NativeCoroutinesアノテーションを追加します。これにより、プラグインがiOSでの正しいFlow処理をサポートするための適切なコードを生成することを保証します。kotlinimport com.rickclephas.kmp.nativecoroutines.NativeCoroutines class Greeting { // ... @NativeCoroutines fun greet(): Flow<String> = flow { // ... } }
XcodeでSPMを使用してライブラリをインポートする
File | Open Project in Xcode に移動します。
Xcodeで、左側のメニューにある
iosAppプロジェクトを右クリックし、Add Package Dependenciesを選択します。検索バーにパッケージ名を入力します。
nonehttps://github.com/rickclephas/KMP-NativeCoroutines.git

- Dependency RuleドロップダウンでExact Version項目を選択し、隣接するフィールドに
1.0.0-ALPHA-45バージョンを入力します。 - Add Packageボタンをクリックします。XcodeはGitHubからパッケージをフェッチし、別のウィンドウを開いてパッケージプロダクトを選択します。
- 表示されているように、「KMPNativeCoroutinesAsync」と「KMPNativeCoroutinesCore」をアプリに追加し、Add Packageをクリックします。

これにより、async/awaitメカニズムを操作するために必要なKMP-NativeCoroutinesパッケージの一部がインストールされます。
KMP-NativeCoroutinesライブラリを使用してFlowを消費する
iosApp/ContentView.swiftで、KMP-NativeCoroutinesのasyncSequence()関数を使用してGreeting().greet()関数にFlowを消費するようにstartObserving()関数を更新します。Swiftfunc startObserving() async { do { let sequence = asyncSequence(for: Greeting().greet()) for try await phrase in sequence { self.greetings.append(phrase) } } catch { print("Failed with error: \(error)") } }ここでのループと
awaitメカニズムは、Flowを反復処理し、Flowが値を放出するたびにgreetingsプロパティを更新するために使用されます。ViewModelが@MainActorアノテーションでマークされていることを確認します。このアノテーションは、ViewModel内のすべての非同期操作がKotlin/Nativeの要件に準拠するためにメインスレッドで実行されることを保証します。Swift// ... import KMPNativeCoroutinesAsync import KMPNativeCoroutinesCore // ... extension ContentView { @MainActor class ViewModel: ObservableObject { @Published var greetings: Array<String> = [] func startObserving() async { do { let sequence = asyncSequence(for: Greeting().greet()) for try await phrase in sequence { self.greetings.append(phrase) } } catch { print("Failed with error: \(error)") } } } }
オプション2. SKIEを構成する
ライブラリを設定するには、shared/build.gradle.ktsにSKIEプラグインを指定し、Sync Gradle Changesボタンをクリックします。
plugins {
id("co.touchlab.skie") version "0.10.6"
}執筆時点での最新であるSKIEのバージョン0.10.6は、最新のKotlinをサポートしていません。これを使用するには、
gradle/libs.versions.tomlファイルでKotlinのバージョンを2.2.10にダウングレードしてください。
SKIEを使用してFlowを消費する
Greeting().greet() Flowを反復処理し、Flowが値を放出するたびにgreetingsプロパティを更新するために、ループとawaitメカニズムを使用します。
ViewModelが@MainActorアノテーションでマークされていることを確認します。 このアノテーションは、ViewModel内のすべての非同期操作がKotlin/Nativeの要件に準拠するためにメインスレッドで実行されることを保証します。
// ...
extension ContentView {
@MainActor
class ViewModel: ObservableObject {
@Published var greetings: [String] = []
func startObserving() async {
for await phrase in Greeting().greet() {
self.greetings.append(phrase)
}
}
}
}ViewModelを消費し、iOSアプリを実行する
iosApp/iOSApp.swiftで、アプリのエントリポイントを更新します。
@main
struct iOSApp: App {
var body: some Scene {
WindowGroup {
ContentView(viewModel: ContentView.ViewModel())
}
}
}IntelliJ IDEAからiosApp構成を実行して、アプリのロジックが同期されていることを確認します。

プロジェクトの最終状態は、異なるコルーチンソリューションを持つGitHubリポジトリの2つのブランチで確認できます。
次のステップ
チュートリアルの最終部では、プロジェクトを締めくくり、次に取るべきステップを確認します。
参照
- サスペンド関数の構成の様々なアプローチを探る。
- Objective-Cフレームワークとライブラリとの相互運用性について詳しく学ぶ。
- ネットワークとデータストレージに関するこのチュートリアルを完了する。
ヘルプを得る
- Kotlin Slack。招待を受けるには、#multiplatformチャンネルに参加してください。
- Kotlin課題トラッカー。新しい課題を報告する。
