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 を使用します。
- 依存性注入を介してプラットフォーム固有のデータベースドライバーを提供するために Koin を使用します。
テンプレートプロジェクトと最終アプリケーションのソースコードは、当社の GitHub リポジトリで確認できます。
プロジェクトを作成する
クイックスタートで、Kotlin Multiplatform 開発のための環境をセットアップするの手順を完了します。
IntelliJ IDEA で、File | New | Project を選択します。
左側のパネルで、Kotlin Multiplatform を選択します(Android Studio では、テンプレートは New Project ウィザードの Generic タブにあります)。
New Project ウィンドウで以下のフィールドを指定します。
- Name: SpaceTutorial
- Group: com.jetbrains
- Artifact: 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 バージョンを確認し、残りを追加します。[versions] agp = "8.7.3" ... coroutinesVersion = "1.10.2" dateTimeVersion = "0.7.1" koin = "4.1.0" ktor = "3.2.3" sqlDelight = "2.1.0" lifecycleViewmodelCompose = "2.9.1" material3 = "1.3.2"
[libraries]
ブロックで、以下のライブラリ参照を追加します。[libraries] ... android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "sqlDelight" } koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" } koin-core = { module = "io.insert-koin:koin-core", 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" } androidx-lifecycle-viewmodel-compose = { group = "androidx.lifecycle", name = "lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelCompose" } androidx-compose-material3 = { module = "androidx.compose.material3:material3", version.ref="material3" }
[plugins]
ブロックで、必要な Gradle プラグインを指定します。[plugins] ... kotlinxSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } sqldelight = { id = "app.cash.sqldelight", version.ref = "sqlDelight" }
依存関係が追加されたら、プロジェクトの再同期を求められます。Sync Gradle Changes ボタンをクリックして Gradle ファイルを同期します。
shared/build.gradle.kts
ファイルの冒頭に、以下の行をplugins {}
ブロックに追加します。kotlinplugins { // ... alias(libs.plugins.kotlinxSerialization) alias(libs.plugins.sqldelight) }
共通ソースセットには、各ライブラリのコアアーティファクトと、ネットワークリクエストおよびレスポンスを処理するために
kotlinx.serialization
を使用する Ktor の シリアライズ機能 が必要です。 iOS および Android のソースセットには、SQLDelight と Ktor のプラットフォームドライバーも必要です。同じ
shared/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) } } }
sourceSets
ブロックの冒頭で、標準 Kotlin ライブラリの実験的な時間 API をオプトインします。kotlinkotlin { // ... sourceSets { all { languageSettings.optIn("kotlin.time.ExperimentalTime") } // ... } }
依存関係が追加されたら、再度 Sync Gradle Changes ボタンをクリックして Gradle ファイルを同期します。
Gradle の同期後、プロジェクトの設定は完了し、コードを記述し始めることができます。
マルチプラットフォームの依存関係に関する詳細ガイドについては、Kotlin Multiplatform ライブラリの依存関係を参照してください。
アプリケーションのデータモデルを作成する
チュートリアルアプリには、ネットワーキングサービスとキャッシュサービスのファサードとして公開されている SpaceXSDK
クラスが含まれます。 アプリケーションのデータモデルには、以下の 3 つのエンティティクラスが含まれます。
- 打ち上げに関する一般情報
- ミッションパッチ画像のリンク
- 打ち上げに関する記事の URL
このデータがすべて最終的に UI に表示されるわけではありません。 シリアライズをデモンストレーションするためにデータモデルを使用しています。 しかし、リンクやパッチをいじって、例をより情報量の多いものに拡張することもできます!
必要なデータクラスを作成します。
shared/src/commonMain/kotlin/com/jetbrains/spacetutorial
ディレクトリにentity
パッケージを作成し、その中にEntity.kt
ファイルを作成します。- 基本的なエンティティのすべてのデータクラスを宣言します。
各シリアライズ可能なクラスは、@Serializable
アノテーションでマークする必要があります。kotlinx.serialization
プラグインは、アノテーション引数にシリアライザへのリンクを明示的に渡さない限り、@Serializable
クラスのデフォルトのシリアライザを自動的に生成します。
@SerialName
アノテーションを使用すると、フィールド名を再定義でき、データクラスのプロパティをより読みやすい識別子でアクセスするのに役立ちます。
SQLDelight を設定し、キャッシュロジックを実装する
SQLDelight を設定する
SQLDelight ライブラリは、SQL クエリから型安全な Kotlin データベース API を生成できます。コンパイル中、ジェネレーターは SQL クエリを検証し、共有モジュールで使用できる Kotlin コードに変換します。
SQLDelight の依存関係はすでにプロジェクトに含まれています。ライブラリを設定するには、shared/build.gradle.kts
ファイルを開き、最後に sqldelight {}
ブロックを追加します。このブロックには、データベースとそのパラメータのリストが含まれています。
sqldelight {
databases {
create("AppDatabase") {
packageName.set("com.jetbrains.spacetutorial.cache")
}
}
}
packageName
パラメータは、生成される Kotlin ソースのパッケージ名を指定します。
プロンプトが表示されたら Gradle プロジェクトファイルを同期するか、ダブル を押して Sync All Gradle, Swift Package Manager projects を検索します。
.sq
ファイルを操作するために、公式の SQLDelight プラグインをインストールすることを検討してください。
データベース API を生成する
まず、必要なすべての SQL クエリを含む .sq
ファイルを作成します。デフォルトでは、SQLDelight プラグインはソースセットの sqldelight
フォルダー内で .sq
ファイルを検索します。
shared/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 );
テーブルにデータを挿入するための
insertLaunch
関数を追加します。textinsertLaunch: INSERT INTO Launch(flightNumber, missionName, details, launchSuccess, launchDateUTC, patchUrlSmall, patchUrlLarge, articleUrl) VALUES(?, ?, ?, ?, ?, ?, ?, ?);
テーブルのデータをクリアするための
removeAllLaunches
関数を追加します。textremoveAllLaunches: DELETE FROM Launch;
データ取得のための
selectAllLaunchesInfo
関数を宣言します。textselectAllLaunchesInfo: SELECT Launch.* FROM Launch;
対応する
AppDatabase
インターフェース(後でデータベースドライバーで初期化する)を生成します。 そのためには、ターミナルで以下のコマンドを実行します。shell./gradlew generateCommonMainAppDatabaseInterface
生成された Kotlin コードは
shared/build/generated/sqldelight
ディレクトリに保存されます。
プラットフォーム固有のデータベースドライバーのファクトリを作成する
AppDatabase
インターフェースを初期化するためには、SqlDriver
インスタンスを渡します。 SQLDelight は SQLite ドライバーの複数のプラットフォーム固有の実装を提供するため、これらのインスタンスを各プラットフォームで個別に作成する必要があります。
expected および actual インターフェースでこれを実現できますが、このプロジェクトでは Koin を使用して Kotlin Multiplatform で依存性注入を試します。
データベースドライバー用のインターフェースを作成します。そのためには、
shared/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 用にこのインターフェースを実装するクラスを作成します。
shared/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
クラスを作成します。
共通ソースセット
shared/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 に接続するクラスを作成します。
shared/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
プロパティを初期化し、格納します。このコードは、Ktor の
ContentNegotiation
プラグインを使用して、GET
リクエストの結果をデシリアライズします。このプラグインは、リクエストとレスポンスのペイロードを 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
修飾子が付いています。これは、suspend
関数 HttpClient.get()
の呼び出しが含まれているためです。 get()
関数には、インターネット経由でデータを取得する非同期操作が含まれており、コルーチンまたは別の suspend
関数からのみ呼び出すことができます。ネットワークリクエストは HTTP クライアントのスレッドプールで実行されます。
GET リクエストを送信するための URL は、get()
関数への引数として渡されます。
SDK を構築する
iOS および Android アプリケーションは、共有モジュールを介して SpaceX API と通信します。このモジュールは、公開クラス SpaceXSDK
を提供します。
共通ソースセット
shared/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 設定を自動的に処理するため、shared
モジュールはすでに Android アプリケーションに接続されています。
UI とプレゼンテーションロジックを実装する前に、必要なすべての UI 依存関係を composeApp/build.gradle.kts
ファイルに追加します。
kotlin {
// ...
sourceSets {
androidMain.dependencies {
implementation(libs.androidx.compose.material3)
implementation(libs.koin.androidx.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)
}
// ...
}
}
プロンプトが表示されたら Gradle プロジェクトファイルを同期するか、ダブル を押して Sync All Gradle, Swift Package Manager projects を検索します。
インターネットアクセス許可を追加する
インターネットにアクセスするため、Android アプリケーションには適切な許可が必要です。 composeApp/src/androidMain/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 の依存性注入を使用すると、異なるコンテキストで使用できるモジュール(コンポーネントのセット)を宣言できます。 このプロジェクトでは、2 つのモジュールを作成します。1 つは Android アプリケーション用、もう 1 つは iOS アプリ用です。 次に、対応するモジュールを使用して各ネイティブ UI に対して Koin を開始します。
Android アプリのコンポーネントを含む Koin モジュールを宣言します。
composeApp/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) } } }
作成した
MainApplication
クラスを、AndroidManifest.xml
ファイルの<application>
タグに指定します。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 を使用して打ち上げリストを取得するビューモデルを作成します。次に、マテリアルテーマを設定し、最後に、これらすべてをまとめるコンポーザブル関数を記述します。
composeApp/src/androidMain
ソースセット、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 から受信したデータとリクエストの現在の状態を格納します。このビューモデルのコルーチンスコープで SDK の
getLaunches
関数を呼び出すloadLaunches
関数を追加します。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
オブジェクトが作成されるとすぐに API からデータを要求するように、クラスのinit {}
ブロックにloadLaunches()
呼び出しを追加します。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()) } }
マテリアルテーマを構築する
メインの App()
コンポーザブルは、マテリアルテーマが提供する AppTheme
関数をベースに構築します。
Material Theme Builder を使用して、Compose アプリのテーマを生成できます。 色とフォントを選択し、右下隅にある Export theme をクリックします。
エクスポート画面で、Export ドロップダウンをクリックし、Jetpack Compose (Theme.kt) オプションを選択します。
アーカイブを解凍し、
theme
フォルダーをcomposeApp/src/androidMain/kotlin/com/jetbrains/spacetutorial
ディレクトリにコピーします。theme
パッケージ内の各ファイルで、作成したパッケージを参照するようにpackage
行を変更します。kotlinpackage com.jetbrains.spacetutorial.theme
Color.kt
ファイルに、成功した打ち上げと失敗した打ち上げに使用する 2 つの色変数を追加します。kotlinval app_theme_successful = Color(0xff4BB543) val app_theme_unsuccessful = Color(0xffFC100D)
プレゼンテーションロジックを実装する
アプリケーションのメインの App()
コンポーザブルを作成し、ComponentActivity
クラスからそれを呼び出します。
com.jetbrains.spacetutorial
パッケージにあるtheme
ディレクトリの隣のApp.kt
ファイルを開き、デフォルトのApp()
コンポーザブル関数を置き換えます。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 org.jetbrains.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() } } } } } } }
最後に、
MainActivity
クラスをAndroidManifest.xml
ファイルの<activity>
タグに指定します。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 Theme によって異なります)。
これで、ビジネスロジックが Kotlin Multiplatform モジュールで実装され、UI がネイティブの Jetpack Compose を使用して作成された Android アプリケーションが完成しました。
iOS アプリケーションを作成する
プロジェクトの iOS 部分では、ユーザーインターフェースを構築するために SwiftUI と、Model View View-Model(MVVM) パターンを利用します。
IntelliJ IDEA は、共有モジュールにすでに接続されている iOS プロジェクトを生成します。Kotlin モジュールは、shared/build.gradle.kts
ファイルで指定された名前(baseName = "Shared"
)でエクスポートされ、通常の import Shared
ステートメントを使用してインポートされます。
SQLDelight 用の動的リンクフラグを追加する
デフォルトでは、IntelliJ IDEA は iOS フレームワークの静的リンク用に設定されたプロジェクトを生成します。
iOS でネイティブの SQLDelight ドライバーを使用するためには、Xcode ツールがシステム提供の SQLite バイナリを見つけられるようにする動的リンカーフラグを追加します。
- IntelliJ IDEA で、File | Open Project in Xcode オプションを選択して Xcode でプロジェクトを開きます。
- Xcode で、プロジェクト名をダブルクリックして設定を開きます。
- Build Settings タブに切り替え、Other Linker Flags フィールドを検索します。
- フィールドの値をダブルクリックし、+ をクリックして
-lsqlite3
文字列を追加します。
iOS 依存性注入用の Koin クラスを準備する
Swift コードで Koin クラスと関数を使用するためには、特別な KoinComponent
クラスを作成し、iOS 用の Koin モジュールを宣言します。
shared/src/iosMain/kotlin/
ソースセットに、cache
フォルダーの隣に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
クラスの後ろに、Swift で iOS Koin モジュールを初期化および開始するために使用するinitKoin
関数を追加します。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() ) } }) } }
これで、iOS アプリで Koin モジュールを開始し、共通の SpaceXSDK
クラスでネイティブデータベースドライバーを使用できます。
UI を実装する
まず、リストから項目を表示するための RocketLaunchRow
SwiftUI ビューを作成します。これは HStack
ビューと VStack
ビューに基づいています。データを表示するのに役立つヘルパーを備えた RocketLaunchRow
構造体に対する拡張機能があります。
IntelliJ IDEA で、Project ビューにいることを確認します。
iosApp
フォルダーに、ContentView.swift
の隣に新しい Swift ファイルを作成し、RocketLaunchRow
という名前を付けます。RocketLaunchRow.swift
ファイルを以下のコードで更新します。Swiftimport SwiftUI import Shared 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
ビューに表示されます。これはすでにプロジェクトに含まれています。データを準備および管理する
ViewModel
クラスを含むContentView
クラスへの拡張を作成します。ContentView.swift
ファイルに以下のコードを追加します。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 { }
データをロードする
ビューモデルでロケット打ち上げに関するデータを取得するためには、Multiplatform ライブラリの KoinHelper
クラスのインスタンスが必要です。 これにより、正しいデータベースドライバーで SDK 関数を呼び出すことができます。
ContentView.swift
ファイルで、ViewModel
クラスを拡張して、KoinHelper
オブジェクトとloadLaunches
関数を含めます。Swiftextension ContentView { // ... @MainActor class ViewModel: ObservableObject { // ... let helper: KoinHelper = KoinHelper() init() { self.loadLaunches(forceReload: false) } func loadLaunches(forceReload: Bool) { // TODO: retrieve data } } }
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 Shared @main struct iOSApp: App { init() { KoinHelperKt.doInitKoin() } var body: some Scene { WindowGroup { ContentView(viewModel: .init()) } } }
IntelliJ IDEA で、
iosApp
構成に切り替え、エミュレーターを選択し、実行して結果を確認します。
プロジェクトの最終バージョンは、
final
ブランチで確認できます。
次は何をしますか?
このチュートリアルでは、メインスレッドでの JSON 解析やデータベースへのリクエスト作成など、リソースを大量に消費する可能性のある操作が含まれています。並行コードの書き方やアプリの最適化について学ぶには、コルーチンガイドを参照してください。
さらに、以下の学習資料も確認できます。