Ktor と SQLDelight を使用してマルチプラットフォームアプリを作成する
このチュートリアルではIntelliJ IDEAを使用しますが、Android Studioでも同様に実行できます。どちらのIDEもコア機能とKotlin Multiplatformサポートを共有しています。
このチュートリアルでは、IntelliJ IDEAを使用して、Kotlin MultiplatformでiOSおよびAndroid向けの高度なモバイルアプリケーションを作成する方法を示します。 このアプリケーションは以下を実行します。
- Ktor を使用して、公開されている SpaceX API からインターネット経由でデータを取得する
- SQLDelight を使用して、ローカルデータベースにデータを保存する
- SpaceX ロケットの打ち上げリストを、打ち上げ日、結果、詳細な説明とともに表示する
アプリケーションには、iOSとAndroidの両方のプラットフォームで共有されるコードを含むモジュールが含まれます。ビジネスロジックとデータアクセスレイヤーは共有モジュールで一度だけ実装され、両方のアプリケーションのUIはネイティブになります。
プロジェクトでは、以下のマルチプラットフォームライブラリを使用します。
- Ktor をHTTPクライアントとして使用し、インターネット経由でデータを取得します。
kotlinx.serialization
を使用して、JSONレスポンスをエンティティクラスのオブジェクトにデシリアライズします。kotlinx.coroutines
を使用して、非同期コードを記述します。- SQLDelight を使用して、SQLクエリからKotlinコードを生成し、型安全なデータベースAPIを作成します。
- Koin を使用して、依存性注入(DI)を介してプラットフォーム固有のデータベースドライバーを提供します。
テンプレートプロジェクトと最終アプリケーションのソースコードは、当社の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.6.2" 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) } } }
依存関係を追加したら、再度Sync Gradle ChangesボタンをクリックしてGradleファイルを同期します。
Gradleの同期後、プロジェクトの設定は完了し、コードを記述し始めることができます。
マルチプラットフォームの依存関係に関する詳細ガイドについては、Kotlin Multiplatformライブラリの依存関係を参照してください。
アプリケーションのデータモデルを作成する
チュートリアルアプリには、ネットワーキングサービスとキャッシュサービスのファサードとして公開されているSpaceXSDK
クラスが含まれます。 アプリケーションのデータモデルには、以下の3つのエンティティクラスが含まれます。
- 打ち上げに関する一般情報
- ミッションパッチ画像のリンク
- 打ち上げに関する記事のURL
このデータがすべて最終的にUIに表示されるわけではありません。 シリアライズをデモンストレーションするためにデータモデルを使用しています。 しかし、リンクやパッチをいじって、例をより情報量の多いものに拡張することもできます!
必要なデータクラスを作成します。
shared/src/commonMain/kotlin/com/jetbrains/spacetutorial
ディレクトリに、entity
パッケージを作成し、その中にEntity.kt
ファイルを作成します。基本的なエンティティのすべてのデータクラスを宣言します。
kotlin
各シリアライズ可能なクラスは、@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とプレゼンテーションロジックを実装する前に、composeApp/build.gradle.kts
ファイルに必要なすべてのUI依存関係を追加します。
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`プロトコルに準拠](https://developer.apple.com/documentation/swift/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解析やデータベースへのリクエスト作成など、リソースを大量に消費する可能性のある操作が含まれています。並行コードの書き方やアプリの最適化について学ぶには、コルーチンガイドを参照してください。
さらに、以下の学習資料も確認できます。