KtorとSQLDelightを使用したマルチプラットフォームアプリの作成
このチュートリアルではIntelliJ IDEAを使用しますが、Android Studioでも同様に進めることができます。両方のIDEは共通のコア機能とKotlin Multiplatformサポートを共有しています。
このチュートリアルでは、IntelliJ IDEAを使用して、Kotlin Multiplatformを用いたiOSおよびAndroid向けの高度なモバイルアプリケーションを作成する方法を説明します。 このアプリケーションでは以下のことを行います。
- Ktorを使用して、パブリックな SpaceX API からインターネット経由でデータを取得する。
- SQLDelightを使用して、ローカルデータベースにデータを保存する。
- SpaceXのロケット打ち上げリストを、打ち上げ日、結果、および詳細な説明とともに表示する。
このアプリケーションには、iOSとAndroidの両方のプラットフォームで共有されるコードを含むモジュールが含まれます。ビジネスロジックとデータアクセスレイヤーは共有モジュールに一度だけ実装され、両方のアプリケーションのUIはネイティブで実装されます。

プロジェクトでは、以下のマルチプラットフォームライブラリを使用します。
- インターネット経由でデータを取得するためのHTTPクライアントとしての Ktor。
- JSONレスポンスをエンティティクラスのオブジェクトにデシリアライズするための
kotlinx.serialization。 - 非同期コードを記述するための
kotlinx.coroutines。 - SQLクエリからKotlinコードを生成し、型安全なデータベースAPIを作成するための SQLDelight。
- 依存性の注入(Dependency Injection)を介してプラットフォーム固有のデータベースドライバーを提供するための Koin。
テンプレートプロジェクト および 最終的なアプリケーション のソースコードは、GitHubリポジトリで見つけることができます。
プロジェクトの作成
クイックスタートの手順に従って、Kotlin Multiplatform開発のための環境構築を完了してください。
IntelliJ IDEAで、File | New | Project を選択します。
左側のパネルで Kotlin Multiplatform を選択します(Android Studioの場合、テンプレートは New Project ウィザードの Generic タブにあります)。
New Project ウィンドウで以下のフィールドを指定します。
- Name: SpaceTutorial
- Project ID: com.jetbrains.spacetutorial
Android と iOS のターゲットを選択します。
iOSについては、Do not share UI オプションを選択します。両方のプラットフォームでネイティブUIを実装します。
すべてのフィールドとターゲットを指定したら、Create をクリックします。

Gradle依存関係の追加
共有モジュールにマルチプラットフォームライブラリを追加するには、build.gradle.kts ファイル内の関連するソースセットの dependencies {} ブロックに依存関係の指示(implementation)を追加する必要があります。
kotlinx.serialization と SQLDelight ライブラリの両方は、追加の設定も必要です。
gradle/libs.versions.toml ファイルのバージョンカタログ内の行を変更または追加して、必要なすべての依存関係を反映させます。
[versions]ブロックで、AGP(Android Gradle Plugin)のバージョンを確認し、残りを追加します。toml[versions] agp = "9.0.1" material3 = "1.11.0-alpha07" # ... coroutinesVersion = "1.10.2" dateTimeVersion = "0.7.1" koin = "4.1.0" ktor = "3.3.3" sqlDelight = "2.2.1"[libraries]ブロックに、以下のライブラリ参照を追加します。[libraries] ... android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqlDelight" } koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" } kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutinesVersion" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "dateTimeVersion" } ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" } ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" } ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } native-driver = { module = "app.cash.sqldelight:native-driver", version.ref = "sqlDelight" } runtime = { module = "app.cash.sqldelight:runtime", version.ref = "sqlDelight" }[plugins]ブロックで、必要なGradleプラグインを指定します。toml[plugins] # ... kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqlDelight" }バージョンカタログが更新されると、プロジェクトの再同期を促されます。 Sync Gradle Changes ボタンをクリックして、Gradleファイルを同期します。

sharedLogic/build.gradle.ktsファイルの冒頭にあるplugins {}ブロックに、以下の行を追加します。kotlinplugins { // ... alias(libs.plugins.kotlinxSerialization) alias(libs.plugins.sqldelight) }共通ソースセット(common source set)には、各ライブラリのコアアーティファクトと、ネットワークリクエストおよびレスポンスの処理に
kotlinx.serializationを使用するためのKtor シリアライゼーション機能が必要です。 また、iOSおよびAndroidソースセットには、SQLDelightとKtorのプラットフォームドライバーが必要です。同じ
sharedLogic/build.gradle.ktsファイルに、必要なすべての依存関係を追加します。kotlinkotlin { // ... sourceSets { commonMain.dependencies { implementation(libs.kotlinx.coroutines.core) implementation(libs.ktor.client.core) implementation(libs.ktor.client.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.runtime) implementation(libs.kotlinx.datetime) implementation(libs.koin.core) } androidMain.dependencies { implementation(libs.ktor.client.android) implementation(libs.android.driver) } iosMain.dependencies { implementation(libs.ktor.client.darwin) implementation(libs.native.driver) } } }依存関係を指定したら、Sync Gradle Changes ボタンをもう一度クリックしてGradleファイルを更新します。
Gradleの同期が完了したら、プロジェクトの設定は終了です。コードの記述を開始できます。
マルチプラットフォームの依存関係に関する詳細なガイドについては、Kotlin Multiplatformライブラリへの依存関係 を参照してください。
アプリケーションデータモデルの作成
チュートリアルアプリには、ネットワーキングサービスとキャッシュサービスを抽象化するファサードとして、パブリックな SpaceXSDK クラスが含まれます。 アプリケーションデータモデルには、以下の情報を持つ3つのエンティティクラスがあります。
- 打ち上げに関する一般情報
- ミッションパッチの画像へのリンク
- 打ち上げに関連する記事のURL
このチュートリアルの最後までには、これらすべてのデータがUIに表示されるわけではありません。 データモデルはシリアライゼーションを実演するために使用しています。 しかし、リンクやパッチを使って、この例をより情報量の多いものに拡張して遊んでみることもできます!
必要なデータクラスを作成します。
sharedLogic/src/commonMain/kotlin/com/jetbrains/spacetutorialディレクトリにentityパッケージを作成し、そのパッケージの中にEntity.ktファイルを作成します。基本的なエンティティのすべてのデータクラスを宣言します。
kotlin
各シリアル化可能なクラスには @Serializable アノテーションを付ける必要があります。kotlinx.serialization プラグインは、アノテーションの引数でシリアライザーへのリンクを明示的に渡さない限り、@Serializable クラス用のデフォルトシリアライザーを自動的に生成します。
@SerialName アノテーションを使用するとフィールド名を再定義でき、より読みやすい識別子を使用してデータクラスのプロパティにアクセスするのに役立ちます。
SQLDelightの設定とキャッシュロジックの実装
SQLDelightライブラリを使用すると、SQLクエリから型安全なKotlinデータベースAPIを生成できます。コンパイル中にジェネレーターはSQLクエリを検証し、共有モジュールで使用できるKotlinコードに変換します。
SQLDelightの設定
SQLDelightの依存関係はすでにプロジェクトに含まれています。 ライブラリを設定するには、sharedLogic/build.gradle.kts ファイルを開き、最後に sqldelight {} ブロックを追加します。 このブロックには、データベースのリストとそのパラメータが含まれます。
sqldelight {
databases {
create("AppDatabase") {
packageName.set("com.jetbrains.spacetutorial.cache")
}
}
}packageName パラメータは、生成されるKotlinソースのパッケージ名を指定します。
プロンプトが表示されたらGradleプロジェクトファイルを同期するか、 を2回押して Sync All Gradle, Swift Package Manager projects を検索して実行します。
.sqファイルを扱うために、公式の SQLDelightプラグイン をインストールすることを検討してください。
データベースAPIの生成
まず、必要なすべてのSQLクエリを含む .sq ファイルを作成します。デフォルトでは、SQLDelightプラグインはソースセットの sqldelight フォルダ内にある .sq ファイルを探します。
sharedLogic/src/commonMainディレクトリに新しいsqldelightディレクトリを作成します。sqldelightディレクトリの中に、パッケージ用のネストされたディレクトリを作成するために、com/jetbrains/spacetutorial/cacheという名前の新しいディレクトリを作成します。cacheディレクトリの中に、AppDatabase.sqファイルを作成します(build.gradle.ktsファイルで指定したデータベースと同じ名前にします)。 アプリケーションのすべてのSQLクエリはこのファイルに保存されます。データベースには、打ち上げに関するデータを持つテーブルが含まれます。 テーブルを作成するための以下のコードを
AppDatabase.sqファイルに追加し、後で使用するいくつかの関数を定義します。textimport kotlin.Boolean; CREATE TABLE Launch ( flightNumber INTEGER NOT NULL, missionName TEXT NOT NULL, details TEXT, launchSuccess INTEGER AS Boolean DEFAULT NULL, launchDateUTC TEXT NOT NULL, patchUrlSmall TEXT, patchUrlLarge TEXT, articleUrl TEXT ); -- 'Launch' テーブルにデータを挿入します insertLaunch: INSERT INTO Launch(flightNumber, missionName, details, launchSuccess, launchDateUTC, patchUrlSmall, patchUrlLarge, articleUrl) VALUES(?, ?, ?, ?, ?, ?, ?, ?); -- 'Launch' テーブルからすべてのデータを消去します removeAllLaunches: DELETE FROM Launch; -- すべての打ち上げに関する情報を取得します selectAllLaunchesInfo: SELECT Launch.* FROM Launch;対応する
AppDatabaseインターフェースを生成します(これについては後でデータベースドライバーを使用して初期化します)。 そのためには、プロジェクトのルートにあるターミナルで以下のコマンドを実行します。shell./gradlew generateCommonMainAppDatabaseInterface生成されたKotlinコードは
sharedLogic/build/generated/sqldelightディレクトリに保存されます。
プラットフォーム固有のデータベースドライバー用のファクトリの作成
AppDatabase インターフェースを初期化するために、SqlDriver インスタンスを渡します。 SQLDelightはSQLiteドライバーの複数のプラットフォーム固有の実装を提供しているため、各プラットフォームごとに個別にインスタンスを作成する必要があります。
これは expect/actualインターフェース を使用して実現することもできますが、このプロジェクトでは Kotlin Multiplatform で依存性の注入を試すために Koin を使用します。
データベースドライバー用のインターフェースを作成します。そのためには、
sharedLogic/src/commonMain/kotlin/com/jetbrains/spacetutorial/ディレクトリにcacheパッケージを作成します。cacheパッケージ内にDatabaseDriverFactoryインターフェースを作成します。kotlinpackage com.jetbrains.spacetutorial.cache import app.cash.sqldelight.db.SqlDriver interface DatabaseDriverFactory { fun createDriver(): SqlDriver }Android 用にこのインターフェースを実装するクラスを作成します。
sharedLogic/src/androidMain/kotlinディレクトリにcom.jetbrains.spacetutorial.cacheパッケージを作成し、その中にDatabaseDriverFactory.ktファイルを作成します。Android では、SQLiteドライバーは
AndroidSqliteDriverクラスによって実装されます。DatabaseDriverFactory.ktファイルで、データベース情報とコンテキストへのリンクをAndroidSqliteDriverクラスのコンストラクタに渡します。kotlinpackage com.jetbrains.spacetutorial.cache import android.content.Context import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.android.AndroidSqliteDriver class AndroidDatabaseDriverFactory(private val context: Context) : DatabaseDriverFactory { override fun createDriver(): SqlDriver { return AndroidSqliteDriver(AppDatabase.Schema, context, "launch.db") } }iOS 用には、
shared/src/iosMain/kotlin/com/jetbrains/spacetutorial/ディレクトリにcacheパッケージを作成します。cacheパッケージ内にDatabaseDriverFactory.ktファイルを作成し、このコードを追加します。kotlinpackage com.jetbrains.spacetutorial.cache import app.cash.sqldelight.db.SqlDriver import app.cash.sqldelight.driver.native.NativeSqliteDriver class IOSDatabaseDriverFactory : DatabaseDriverFactory { override fun createDriver(): SqlDriver { return NativeSqliteDriver(AppDatabase.Schema, "launch.db") } }
これらのファクトリは、後でプロジェクトのプラットフォーム固有の部分で使用します。
キャッシュの実装
ここまでで、プラットフォームデータベースドライバー用のファクトリと、データベース操作を実行するための AppDatabase インターフェースを追加しました。 次に、AppDatabase インターフェースをラップし、キャッシュロジックを含む Database クラスを作成します。
共通ソースセット
sharedLogic/src/commonMain/kotlinのcom.jetbrains.spacetutorial.cacheパッケージに新しいDatabaseクラスを作成します。これには両方のプラットフォームに共通のロジックが含まれます。AppDatabaseにドライバーを提供するために、抽象的なDatabaseDriverFactoryインスタンスをDatabaseクラスのコンストラクタに渡します。kotlinpackage com.jetbrains.spacetutorial.cache internal class Database(databaseDriverFactory: DatabaseDriverFactory) { private val database = AppDatabase(databaseDriverFactory.createDriver()) private val dbQuery = database.appDatabaseQueries }このクラスの 可視性 は internal に設定されており、マルチプラットフォームモジュール内からのみアクセス可能であることを意味します。
Databaseクラスの中に、いくつかのデータ処理操作を実装します。 まず、すべてのロケット打ち上げのリストを返すgetAllLaunches関数を作成します。mapLaunchSelecting関数は、データベースクエリの結果をRocketLaunchオブジェクトにマッピングするために使用されます。kotlinimport com.jetbrains.spacetutorial.entity.Links import com.jetbrains.spacetutorial.entity.Patch import com.jetbrains.spacetutorial.entity.RocketLaunch internal class Database(databaseDriverFactory: DatabaseDriverFactory) { // ... internal fun getAllLaunches(): List<RocketLaunch> { return dbQuery.selectAllLaunchesInfo(::mapLaunchSelecting).executeAsList() } private fun mapLaunchSelecting( flightNumber: Long, missionName: String, details: String?, launchSuccess: Boolean?, launchDateUTC: String, patchUrlSmall: String?, patchUrlLarge: String?, articleUrl: String? ): RocketLaunch { return RocketLaunch( flightNumber = flightNumber.toInt(), missionName = missionName, details = details, launchDateUTC = launchDateUTC, launchSuccess = launchSuccess, links = Links( patch = Patch( small = patchUrlSmall, large = patchUrlLarge ), article = articleUrl ) ) } }データベースをクリアして新しいデータを挿入するための
clearAndCreateLaunches関数を追加します。kotlininternal class Database(databaseDriverFactory: DatabaseDriverFactory) { // ... internal fun clearAndCreateLaunches(launches: List<RocketLaunch>) { dbQuery.transaction { dbQuery.removeAllLaunches() launches.forEach { launch -> dbQuery.insertLaunch( flightNumber = launch.flightNumber.toLong(), missionName = launch.missionName, details = launch.details, launchSuccess = launch.launchSuccess ?: false, launchDateUTC = launch.launchDateUTC, patchUrlSmall = launch.links.patch?.small, patchUrlLarge = launch.links.patch?.large, articleUrl = launch.links.article ) } } } }
APIサービスの実装
インターネット経由でデータを取得するために、SpaceXパブリックAPI と、v5/launches エンドポイントからすべての打ち上げリストを取得する単一のメソッドを使用します。
アプリケーションをAPIに接続するクラスを作成します。
sharedLogic/src/commonMain/kotlin/com/jetbrains/spacetutorial/ディレクトリにnetworkパッケージを作成します。networkディレクトリの中にSpaceXApiクラスを作成します。kotlinpackage com.jetbrains.spacetutorial.network import io.ktor.client.HttpClient import io.ktor.client.plugins.contentnegotiation.ContentNegotiation import io.ktor.serialization.kotlinx.json.json import kotlinx.serialization.json.Json class SpaceXApi { private val httpClient = HttpClient { install(ContentNegotiation) { json(Json { ignoreUnknownKeys = true useAlternativeNames = false }) } } }このクラスはネットワークリクエストを実行し、JSONレスポンスを
com.jetbrains.spacetutorial.entityパッケージのエンティティにデシリアライズします。 KtorのHttpClientインスタンスがhttpClientプロパティを初期化して保持します。このコードでは、
GETリクエストの結果をデシリアライズするために Ktor のContentNegotiationプラグインを使用しています。このプラグインは、リクエストとレスポンスのペイロードを JSON として処理し、必要に応じてシリアライズおよびデシリアライズします。ロケット打ち上げのリストを返すデータ取得関数を宣言します。
kotlinimport com.jetbrains.spacetutorial.entity.RocketLaunch import io.ktor.client.request.get import io.ktor.client.call.body class SpaceXApi { // ... suspend fun getAllLaunches(): List<RocketLaunch> { return httpClient.get("https://api.spacexdata.com/v5/launches").body() } }
getAllLaunches 関数には suspend 修飾子が付いています。これは、サスペンド関数である HttpClient.get() の呼び出しを含んでいるためです。 HttpClient.get() 関数にはインターネット経由でデータを取得するための非同期操作が含まれており、コルーチンまたは別のサスペンド関数からしか呼び出すことができません。ネットワークリクエストはHTTPクライアントのスレッドプールで実行されます。
GETリクエストを送信するためのURLが、get() 関数の引数として渡されます。
SDKの構築
iOSおよびAndroidアプリケーションは、共有モジュールを通じてSpaceX APIと通信します。共有モジュールは公開クラス SpaceXSDK を提供します。
共通ソースセット
sharedLogic/src/commonMain/kotlinのcom.jetbrains.spacetutorialパッケージにSpaceXSDKクラスを作成します。 このクラスは、DatabaseクラスとSpaceXApiクラスのファサードになります。Databaseクラスのインスタンスを作成するために、DatabaseDriverFactoryインスタンスを提供します。kotlinpackage com.jetbrains.spacetutorial import com.jetbrains.spacetutorial.cache.Database import com.jetbrains.spacetutorial.cache.DatabaseDriverFactory import com.jetbrains.spacetutorial.network.SpaceXApi class SpaceXSDK(databaseDriverFactory: DatabaseDriverFactory, val api: SpaceXApi) { private val database = Database(databaseDriverFactory) }SpaceXSDKクラスのコンストラクタを通じて、プラットフォーム固有のコードで正しいデータベースドライバーを注入します。作成したデータベースとAPIを使用して打ち上げリストを取得する
getLaunches関数を追加します。kotlinimport com.jetbrains.spacetutorial.entity.RocketLaunch class SpaceXSDK(databaseDriverFactory: DatabaseDriverFactory, val api: SpaceXApi) { // ... @Throws(Exception::class) suspend fun getLaunches(forceReload: Boolean): List<RocketLaunch> { val cachedLaunches = database.getAllLaunches() return if (cachedLaunches.isNotEmpty() && !forceReload) { cachedLaunches } else { api.getAllLaunches().also { database.clearAndCreateLaunches(it) } } } }
このクラスには、すべての打ち上げ情報を取得するための関数が1つ含まれています。forceReload の値に応じて、キャッシュされた値を返すか、インターネットからデータをロードしてからその結果でキャッシュを更新します。キャッシュされたデータがない場合は、forceReload フラグの値に関係なくインターネットからデータをロードします。
SDKのクライアントは、forceReload フラグを使用して最新の打ち上げ情報をロードでき、ユーザー向けのプル・トゥ・リフレッシュ(引っ張って更新)ジェスチャなどを実現できます。
Kotlinのすべての例外は非検査(unchecked)ですが、Swiftには検査例外(checked errors)しかありません(詳細は Swift/Objective-Cとの相互運用性 を参照)。したがって、Swiftコードに期待される例外を認識させるために、Swiftから呼び出されるKotlin関数には、潜在的な例外クラスのリストを指定する @Throws アノテーションを付ける必要があります。
Androidアプリケーションの作成
IntelliJ IDEAが初期のGradle設定を自動で行うため、sharedUI モジュールと sharedLogic モジュールはすでにAndroidアプリケーション(androidApp)に接続されています。
UIとプレゼンテーションロジックを実装する前に、sharedUI/build.gradle.kts ファイルに Koin Android の依存関係を追加します。
kotlin {
// ...
sourceSets {
androidMain.dependencies {
implementation(libs.koin.androidx.compose)
}
}
}プロンプトが表示されたらGradleプロジェクトファイルを同期するか、 を2回押して Sync All Gradle, Swift Package Manager projects を検索して実行します。
androidApp へのインターネットアクセス権限の追加
インターネットにアクセスするには、Androidアプリケーションに適切な権限が必要です。 androidApp/src/main/AndroidManifest.xml ファイルに、<uses-permission> タグを追加します。
<?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>依存性の注入コードの追加
Koinの依存性の注入を使用すると、異なるコンテキストで使用できるモジュール(コンポーネントのセット)を宣言できます。 このプロジェクトでは、Androidアプリケーション用とiOSアプリ用の2つのモジュールを作成します。 次に、対応するモジュールを使用して、各ネイティブUIに対してKoinを開始します。
Androidアプリのコンポーネントを含むKoinモジュールを宣言します。
sharedUI/src/androidMain/kotlinディレクトリのcom.jetbrains.spacetutorialパッケージにAppModule.ktファイルを作成します。そのファイルで、
SpaceXApiクラス用とSpaceXSDKクラス用の2つの シングルトン としてモジュールを宣言します。kotlinpackage com.jetbrains.spacetutorial import com.jetbrains.spacetutorial.cache.AndroidDatabaseDriverFactory import com.jetbrains.spacetutorial.network.SpaceXApi import org.koin.android.ext.koin.androidContext import org.koin.dsl.module val appModule = module { single<SpaceXApi> { SpaceXApi() } single<SpaceXSDK> { SpaceXSDK( databaseDriverFactory = AndroidDatabaseDriverFactory( androidContext() ), api = get() ) } }SpaceXSDKクラスのコンストラクタには、プラットフォーム固有のAndroidDatabaseDriverFactoryクラスが注入されます。get()関数はモジュール内の依存関係を解決します。SpaceXSDK()のapiパラメータの代わりに、Koinは先ほど宣言したSpaceXApiシングルトンを渡します。Koinモジュールを開始するカスタム
Applicationクラスを作成します。作成した
AppModule.ktファイルの隣に、Application.ktファイルを作成し、以下のコードを記述します。modules()関数の呼び出しで、宣言したモジュールを指定します。kotlinpackage com.jetbrains.spacetutorial import android.app.Application import org.koin.android.ext.koin.androidContext import org.koin.core.context.GlobalContext.startKoin class MainApplication : Application() { override fun onCreate() { super.onCreate() startKoin { androidContext(this@MainApplication) modules(appModule) } } }AndroidManifest.xmlファイルの<application>タグで、作成したMainApplicationクラスを指定します。xml<manifest xmlns:android="http://schemas.android.com/apk/res/android"> ... <application ... android:name="com.jetbrains.spacetutorial.MainApplication"> ... </application> </manifest>
これで、プラットフォーム固有のデータベースドライバーによって提供される情報を使用するUIを実装する準備が整いました。
打ち上げリストを表示するビューモデルの準備
Jetpack ComposeとMaterial 3を使用してAndroidのUIを実装します。まず、SDKを使用して打ち上げリストを取得するビューモデルを作成します。次にMaterialテーマを設定し、最後にすべてを統合するコンポーザブル(composable)関数を作成します。
sharedUI/src/androidMain/kotlinディレクトリのcom.jetbrains.spacetutorialパッケージにRocketLaunchViewModel.ktファイルを作成します。kotlinpackage com.jetbrains.spacetutorial import androidx.compose.runtime.State import androidx.compose.runtime.mutableStateOf import androidx.lifecycle.ViewModel import com.jetbrains.spacetutorial.entity.RocketLaunch class RocketLaunchViewModel(private val sdk: SpaceXSDK) : ViewModel() { private val _state = mutableStateOf(RocketLaunchScreenState()) val state: State<RocketLaunchScreenState> = _state } data class RocketLaunchScreenState( val isLoading: Boolean = false, val launches: List<RocketLaunch> = emptyList() )RocketLaunchScreenStateインスタンスは、SDKから受信したデータとリクエストの現在の状態を保存します。RocketLaunchViewModelクラスにloadLaunches関数を追加します。この関数は、このビューモデルのコルーチンスコープでSDKのgetLaunches関数を呼び出します。kotlinimport androidx.lifecycle.viewModelScope import kotlinx.coroutines.launch class RocketLaunchViewModel(private val sdk: SpaceXSDK) : ViewModel() { //... fun loadLaunches() { viewModelScope.launch { _state.value = _state.value.copy(isLoading = true, launches = emptyList()) try { val launches = sdk.getLaunches(forceReload = true) _state.value = _state.value.copy(isLoading = false, launches = launches) } catch (e: Exception) { _state.value = _state.value.copy(isLoading = false, launches = emptyList()) } } } }RocketLaunchViewModelクラスの中に、RocketLaunchViewModelオブジェクトが作成されたらすぐにAPIにデータを要求するために、loadLaunches()の呼び出しを含むinit {}ブロックを追加します。kotlinclass RocketLaunchViewModel(private val sdk: SpaceXSDK) : ViewModel() { // ... init { loadLaunches() } }次に、
AppModule.ktファイルで、Koinモジュールにビューモデルを指定します。kotlinimport org.koin.core.module.dsl.viewModel val appModule = module { // ... viewModel { RocketLaunchViewModel(sdk = get()) } }
Materialテーマの構築
Materialテーマによって提供される AppTheme 関数の周りに、メインの App() コンポーザブルを構築します。
Material Theme Builder を使用して、Composeアプリ用のテーマを生成できます。色とフォントを選択し、右下の Export theme をクリックします。
エクスポート画面で、Export ドロップダウンをクリックし、Jetpack Compose (Theme.kt) オプションを選択します。
アーカイブを解凍し、
themeフォルダをsharedUI/src/androidMain/kotlin/com/jetbrains/spacetutorialディレクトリにコピーします。
themeパッケージ内の各ファイルで、package行を作成したパッケージを参照するように変更します。kotlinpackage com.jetbrains.spacetutorial.themeColor.ktファイルに、成功した打ち上げと失敗した打ち上げに使用する色のための2つの変数を追加します。kotlinval app_theme_successful = Color(0xff4BB543) val app_theme_unsuccessful = Color(0xffFC100D)
プレゼンテーションロジックの実装
アプリケーションのメインとなる App() コンポーザブルを作成し、それを ComponentActivity クラスから呼び出します。
Android UIは共有されないため、
sharedUIモジュールからcommonMainおよびcommonTestソースセットを削除します。sharedUI/src/androidApp/kotlin/com/jetbrains/spacetutorialディレクトリにApp.ktファイルを作成します。App.ktファイルを開き、以下のコードを挿入します。kotlinpackage com.jetbrains.spacetutorial import androidx.compose.material3.pulltorefresh.rememberPullToRefreshState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.ui.tooling.preview.Preview import org.koin.androidx.compose.koinViewModel import androidx.compose.material3.ExperimentalMaterial3Api @OptIn( ExperimentalMaterial3Api::class ) @Composable @Preview fun App() { val viewModel = koinViewModel<RocketLaunchViewModel>() val state by remember { viewModel.state } val coroutineScope = rememberCoroutineScope() var isRefreshing by remember { mutableStateOf(false) } val pullToRefreshState = rememberPullToRefreshState() }ここでは、Koin ViewModel API を使用して、Android Koinモジュールで宣言した
viewModelを参照しています。次に、読み込み画面、打ち上げ結果の列、およびプル・トゥ・リフレッシュ・アクションを実装するUIコードを追加します。
kotlinpackage com.jetbrains.spacetutorial import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import com.jetbrains.spacetutorial.entity.RocketLaunch import com.jetbrains.spacetutorial.theme.AppTheme import com.jetbrains.spacetutorial.theme.app_theme_successful import com.jetbrains.spacetutorial.theme.app_theme_unsuccessful import kotlinx.coroutines.launch ... @OptIn( ExperimentalMaterial3Api::class ) @Composable @Preview fun App() { val viewModel = koinViewModel<RocketLaunchViewModel>() val state by remember { viewModel.state } val coroutineScope = rememberCoroutineScope() var isRefreshing by remember { mutableStateOf(false) } val pullToRefreshState = rememberPullToRefreshState() AppTheme { Scaffold( topBar = { TopAppBar( title = { Text( "SpaceX Launches", style = MaterialTheme.typography.headlineLarge ) } ) } ) { padding -> PullToRefreshBox( modifier = Modifier .fillMaxSize() .padding(padding), state = pullToRefreshState, isRefreshing = isRefreshing, onRefresh = { isRefreshing = true coroutineScope.launch { viewModel.loadLaunches() isRefreshing = false } } ) { if (state.isLoading && !isRefreshing) { Column( verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxSize() ) { Text("Loading...", style = MaterialTheme.typography.bodyLarge) } } else { LazyColumn { items(state.launches) { launch: RocketLaunch -> Column(modifier = Modifier.padding(16.dp)) { Text( text = "${launch.missionName} - ${launch.launchYear}", style = MaterialTheme.typography.headlineSmall ) Spacer(Modifier.height(8.dp)) Text( text = if (launch.launchSuccess == true) "Successful" else "Unsuccessful", color = if (launch.launchSuccess == true) app_theme_successful else app_theme_unsuccessful ) Spacer(Modifier.height(8.dp)) val details = launch.details if (details != null && details.isNotBlank()) { Text(details) } } HorizontalDivider() } } } } } } }最後に、
androidApp/src/main/AndroidManifest.xmlの<activity>タグでMainActivityクラスを指定します。xml<manifest xmlns:android="http://schemas.android.com/apk/res/android"> ... <application ... <activity ... android:name="com.jetbrains.spacetutorial.MainActivity"> ... </activity> </application> </manifest>Androidアプリを実行します。実行構成メニューから composeApp を選択し、エミュレーターを選択して、実行ボタンをクリックします。 アプリは自動的にAPIリクエストを実行し、打ち上げリストを表示します(背景色は生成したMaterialテーマによって異なります)。

これで、ビジネスロジックが Kotlin Multiplatform モジュールに実装され、UIがネイティブの Jetpack Compose を使用して作成された Android アプリケーションを作成できました。
iOSアプリケーションの作成
プロジェクトのiOS部分については、ユーザーインターフェースの構築に SwiftUI を使用し、Model View View-Model パターンを利用します。
IntelliJ IDEAは、共有モジュールにすでに接続されたiOSプロジェクトを生成します。Kotlinモジュールは sharedLogic/build.gradle.kts ファイルで指定された名前(baseName = "SharedLogic")でエクスポートされ、通常の import ステートメント import SharedLogic を使用してインポートされます。
SQLDelight用の動的リンクフラグの追加
デフォルトでは、IntelliJ IDEAはiOSフレームワークの静的リンク用に設定されたプロジェクトを生成します。
iOSでネイティブのSQLDelightドライバーを使用するには、Xcodeツールがシステム提供のSQLiteバイナリを見つけられるようにするための動的リンカーフラグを追加します。
IntelliJ IDEAで、File | Open Project in Xcode オプションを選択してプロジェクトをXcodeで開きます。
Xcodeで、プロジェクト名をクリックしてその設定を開きます。
Build Settings タブに切り替え、All リストに切り替えて、Other Linker Flags フィールドを検索します。
フィールドを展開し、Debug フィールドの隣にあるプラス記号を押し、
-lsqlite3文字列を Any Architecture | Any SDK に貼り付けます。Other Linker Flags | Release フィールドについても同様の手順を繰り返します。

IntelliJ IDEAに戻ります。
iOS依存性の注入用のKoinクラスの準備
SwiftコードでKoinのクラスや関数を使用するために、特別な KoinComponent クラスを作成し、iOS用のKoinモジュールを宣言します。
sharedLogic/src/iosMain/kotlin/com/jetbrains/spacetutorialディレクトリにKoinHelper.ktファイルを作成します。SpaceXSDKクラスを遅延Koin注入でラップするKoinHelperクラスを追加します。kotlinpackage com.jetbrains.spacetutorial import org.koin.core.component.KoinComponent import com.jetbrains.spacetutorial.entity.RocketLaunch import org.koin.core.component.inject class KoinHelper : KoinComponent { private val sdk: SpaceXSDK by inject<SpaceXSDK>() suspend fun getLaunches(forceReload: Boolean): List<RocketLaunch> { return sdk.getLaunches(forceReload = forceReload) } }KoinHelperクラスの下にinitKoin()関数を追加します。これは、iOS Koinモジュールを初期化して開始するためにSwiftで使用します。kotlinimport com.jetbrains.spacetutorial.cache.IOSDatabaseDriverFactory import com.jetbrains.spacetutorial.network.SpaceXApi import org.koin.core.context.startKoin import org.koin.dsl.module fun initKoin() { startKoin { modules(module { single<SpaceXApi> { SpaceXApi() } single<SpaceXSDK> { SpaceXSDK( databaseDriverFactory = IOSDatabaseDriverFactory(), api = get() ) } }) } }
これで、共通の SpaceXSDK クラスでネイティブデータベースドライバーを使用するために、iOSアプリでKoinモジュールを開始できるようになりました。
UIの実装
まず、リストのアイテムを表示するための RocketLaunchRow SwiftUIビューを作成します。これは HStack と VStack ビューに基づいています。RocketLaunchRow 構造体には、データを表示するための便利なヘルパーを持つ拡張機能を追加します。
IntelliJ IDEAで、Project ビューにいることを確認します。
iosApp/iosAppフォルダ内のContentView.swiftの隣に、RocketLaunchRowという名前で新しいSwiftファイルを作成します。RocketLaunchRow.swiftファイルを以下のコードで更新します。Swiftimport SwiftUI import SharedLogic struct RocketLaunchRow: View { var rocketLaunch: RocketLaunch var body: some View { HStack() { VStack(alignment: .leading, spacing: 10.0) { Text("\(rocketLaunch.missionName) - \(String(rocketLaunch.launchYear))").font(.system(size: 18)).bold() Text(launchText).foregroundColor(launchColor) Text("Launch year: \(String(rocketLaunch.launchYear))") Text("\(rocketLaunch.details ?? "")") } Spacer() } } } extension RocketLaunchRow { private var launchText: String { if let isSuccess = rocketLaunch.launchSuccess { return isSuccess.boolValue ? "Successful" : "Unsuccessful" } else { return "No data" } } private var launchColor: Color { if let isSuccess = rocketLaunch.launchSuccess { return isSuccess.boolValue ? Color.green : Color.red } else { return Color.gray } } }打ち上げのリストは、プロジェクトにすでに含まれている
ContentViewビューに表示されます。ContentView.swiftファイルで、データを準備して管理するViewModelクラスを持つContentViewクラスの拡張機能を追加します。Swiftextension ContentView { enum LoadableLaunches { case loading case result([RocketLaunch]) case error(String) } @MainActor class ViewModel: ObservableObject { @Published var launches = LoadableLaunches.loading } }ビューモデル(
ContentView.ViewModel)は、Combineフレームワーク を介してビュー(ContentView)と接続します。ContentView.ViewModelクラスはObservableObjectとして宣言されます。launchesプロパティには@Published属性が使用されているため、このプロパティが変更されるたびにビューモデルはシグナルを発行します。
ContentView_Previews構造体を削除します。ビューモデルと互換性のあるプレビューを実装する必要はありません。ContentViewクラスの本体を更新して打ち上げリストを表示し、リロード機能を追加します。- これはUIの下準備です。
loadLaunches関数はチュートリアルの次の段階で実装します。 viewModelプロパティには、ビューモデルを購読するために@ObservedObject属性が付けられています。
swiftstruct ContentView: View { @ObservedObject private(set) var viewModel: ViewModel var body: some View { NavigationView { listView() .navigationBarTitle("SpaceX Launches") .navigationBarItems(trailing: Button("Reload") { self.viewModel.loadLaunches(forceReload: true) }) } } private func listView() -> AnyView { switch viewModel.launches { case .loading: return AnyView(Text("Loading...").multilineTextAlignment(.center)) case .result(let launches): return AnyView(List(launches) { launch in RocketLaunchRow(rocketLaunch: launch) }) case .error(let description): return AnyView(Text(description).multilineTextAlignment(.center)) } } }- これはUIの下準備です。
RocketLaunchクラスはListビューを初期化するためのパラメータとして使用されるため、Identifiableプロトコルに適合 する必要があります。 このクラスにはすでにidという名前のプロパティがあるため、ContentView.swiftの下部に拡張機能を追加するだけで済みます。Swiftextension RocketLaunch: Identifiable { }
データのロード
ビューモデルでロケット打ち上げに関するデータを取得するには、マルチプラットフォームライブラリの KoinHelper クラスのインスタンスが必要です。 これにより、正しいデータベースドライバーを使用してSDK関数を呼び出すことができます。
ContentView.swiftファイルで、ViewModelクラスを拡張してKoinHelperオブジェクトとloadLaunches関数を含めます。Swiftextension ContentView { // ... class ViewModel: ObservableObject { // ... let helper: KoinHelper = KoinHelper() init() { self.loadLaunches(forceReload: false) } func loadLaunches(forceReload: Bool) { // TODO: データを取得する } } }loadLaunches()関数で、KoinHelper.getLaunches()関数(SpaceXSDKクラスへの呼び出しを中継します)を呼び出し、その結果をlaunchesプロパティに保存します。Swiftfunc loadLaunches(forceReload: Bool) { Task { do { self.launches = .loading let launches = try await helper.getLaunches(forceReload: forceReload) self.launches = .result(launches) } catch { self.launches = .error(error.localizedDescription) } } }KotlinモジュールをAppleフレームワークにコンパイルすると、サスペンド関数 は Swift の
async/awaitメカニズムを使用して呼び出すことができます。getLaunches関数は Kotlin で@Throws(Exception::class)アノテーションが付けられているため、Exceptionクラスまたはそのサブクラスのインスタンスである例外はNSErrorとして Swift に伝播されます。 したがって、そのようなすべての例外はloadLaunches()関数でキャッチできます。アプリのエントリーポイントである
iOSApp.swiftファイルに移動し、Koinモジュール、ビュー、およびビューモデルを初期化します。Swiftimport SwiftUI import SharedLogic @main struct iOSApp: App { init() { KoinHelperKt.doInitKoin() } var body: some Scene { WindowGroup { ContentView(viewModel: .init()) } } }IntelliJ IDEAで、iosApp 構成に切り替え、エミュレーターを選択して実行し、結果を確認します。

プロジェクトの最終バージョンは
finalブランチ で見つけることができます。
次のステップ
このチュートリアルでは、JSONの解析やメインスレッドでのデータベースへのリクエストなど、リソース負荷の高い操作が含まれています。並行コードの記述方法やアプリの最適化について学ぶには、コルーチンガイド を参照してください。
また、以下の追加学習資料も確認してください。
