既存のアプリを Room KMP に移行する

1. 始める前に

前提条件

必要なもの

学習内容

  • Android アプリと iOS アプリ間で Room Database を共有する方法。

2. セットアップする

使用を開始するには、以下のステップを実行してください。

  1. 次のターミナル コマンドを使用して、GitHub リポジトリのクローンを作成します。
$ git clone https://github.com/android/codelab-android-kmp.git

または、リポジトリを ZIP ファイルとしてダウンロードすることもできます。

  1. Android Studiomigrate-room プロジェクトを開きます。このプロジェクトには以下のブランチが含まれます。
  • main: このプロジェクトのスターター コードが含まれています。これに変更を加えて Codelab を完了します。
  • end: この Codelab の解答コードが含まれています。

main ブランチから始めて、Codelab の手順に沿って自分のペースで進めることをおすすめします。

  1. 解答コードを確認する場合は、このコマンドを実行します。
$ git clone -b end https://github.com/android/codelab-android-kmp.git

または、解答コードをダウンロードすることもできます。

3. サンプルアプリについて理解する

このチュートリアルでは、ネイティブ フレームワーク(Android では Jetpack Compose、iOS では SwiftUi)で構築された Fruitties サンプル アプリケーションについて説明します。

Fruitties アプリには、主に次の 2 つの機能があります。

  • フルーツ アイテムのリスト。各アイテムには、カートにアイテムを追加するボタンがあります。
  • 上部に表示されるカート。追加されたフルーツの数とその量が表示されます。

4a7f262b015d7f78.png

Android アプリ アーキテクチャ

Android アプリは、公式の Android アーキテクチャ ガイドラインに沿って、明確でモジュール化された構造を維持しています。

KMP 統合前の Android アプリケーションのアーキテクチャ図

iOS アプリのアーキテクチャ

KMP 統合前の iOS アプリケーションのアーキテクチャ図

KMP 共有モジュール

このプロジェクトには、KMP 用の共有モジュールがすでに設定されていますが、現在は空です。プロジェクトに共有モジュールがまだ設定されていない場合は、Kotlin マルチプラットフォームのスタートガイドの Codelab から始めてください。

4. KMP 統合用に Room データベースを準備する

Room データベース コードを Fruitties Android アプリから shared モジュールに移動する前に、アプリが Kotlin マルチプラットフォーム(KMP)Room API と互換性があることを確認する必要があります。このセクションでは、そのプロセスについて説明します。

主な更新点の一つは、Android と iOS の両方に対応した SQLite ドライバの使用です。複数のプラットフォームで Room データベース機能をサポートするには、BundledSQLiteDriver を使用します。このドライバは SQLite をアプリケーションに直接バンドルするため、Kotlin でのマルチプラットフォームの使用に適しています。詳細なガイダンスについては、Room KMP 移行ガイドをご覧ください。

依存関係を更新する

まず、room-runtimesqlite-bundled の依存関係を libs.versions.toml ファイルに追加します。

# Add libraries
[libraries]
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "androidx-room" }

androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "androidx-sqlite" }

次に、これらの依存関係を使用するように :androidApp モジュールの build.gradle.kts を更新し、libs.androidx.room.ktx の使用を削除します。

// Add
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.sqlite.bundled)
// Remove
implementation(libs.androidx.room.ktx)

そして、Android Studio でプロジェクトを同期します。

BundledSQLiteDriver のデータベース モジュールを変更する

次に、Android アプリのデータベース作成ロジックを変更して BundledSQLiteDriver を使用するようにし、Android での機能を維持しながら KMP と互換性を持たせます。

  1. androidApp/src/main/kotlin/com/example/fruitties/kmptutorial/android/di/DatabaseModule.kt にある DatabaseModule.kt ファイルを開きます。
  2. providesAppDatabase メソッドを次のスニペットのように更新します。
import androidx.sqlite.driver.bundled.BundledSQLiteDriver

@Module
@InstallIn(SingletonComponent::class)
internal object DatabaseModule {

...

@Provides
@Singleton
fun providesAppDatabase(@ApplicationContext context: Context): AppDatabase {
    val dbFile = context.getDatabasePath("sharedfruits.db")
    return Room.databaseBuilder<AppDatabase>(context, dbFile.absolutePath)
        .setDriver(BundledSQLiteDriver())
        .build()
}

Android アプリをビルドして実行する

ネイティブ SQLite ドライバをバンドルされたドライバに切り替えたので、データベースを :shared モジュールに移行する前に、アプリがビルドされ、すべてが正しく動作することを確認しましょう。

5. データベース コードを :shared モジュールに移動する

このステップでは、Room データベースの設定を Android アプリから :shared モジュールに移行し、Android と iOS の両方からデータベースにアクセスできるようにします。

:shared モジュールbuild.gradle.kts 構成を更新する

まず、Room マルチプラットフォームの依存関係を使用するように :shared モジュールの build.gradle.kts を更新します。

  1. KSP プラグインと Room プラグインを追加します。
plugins {
   ...
   // TODO add KSP + ROOM plugins
   alias(libs.plugins.ksp)
   alias(libs.plugins.room)
}
  1. room-runtimesqlite-bundled の依存関係を commonMain ブロックに追加します。
sourceSets {
    commonMain {
        // TODO Add KMP dependencies here
        implementation(libs.androidx.room.runtime)
        implementation(libs.androidx.sqlite.bundled)
    }
}
  1. 新しいトップレベルの dependencies ブロックを追加して、プラットフォーム ターゲットごとに KSP 構成を追加します。簡単にするため、ファイルの一番下に追加するだけで構いません。
// Should be its own top level block. For convenience, add at the bottom of the file
dependencies {
   add("kspAndroid", libs.androidx.room.compiler)
   add("kspIosSimulatorArm64", libs.androidx.room.compiler)
   add("kspIosX64", libs.androidx.room.compiler)
   add("kspIosArm64", libs.androidx.room.compiler)
}
  1. また、トップレベルに新しいブロックを追加して、Room スキーマの場所を設定します。
// Should be its own top level block. For convenience, add at the bottom of the file
room {
   schemaDirectory("$projectDir/schemas")
}
  1. プロジェクトを Gradle 同期します。

Room スキーマを :shared モジュールに移動する

androidApp/schemas ディレクトリを、src/ フォルダの横にある :shared モジュールのルート フォルダに移動します。

e1ee37a3f3a10b35.png から

ba3c9eb617828bac.png

DAO とエンティティを移動する

必要な Gradle 依存関係を KMP 共有モジュールに追加したので、DAO とエンティティを :androidApp モジュールから :shared モジュールに移動する必要があります。

具体的には、:shared モジュールの commonMain ソースセット内のそれぞれの場所にファイルを移動する必要があります。

Fruittie モデルを移動する

[Refactor → Move] 機能を使用すると、インポートを中断することなくモジュールを切り替えることができます。

  1. androidApp/src/main/kotlin/.../model/Fruittie.kt ファイルを見つけ、そのファイルを右クリックして、[Refactor → Move](または F6 キー)を選択します。:c893e12b8bf683ae.png
  2. [Move] ダイアログで、[Destination directory] フィールドの横にある ... アイコンを選択します。1d51c3a410e8f2c3.png
  3. [Choose Destination Directory] ダイアログで commonMain ソースセットを選択し、[OK] をクリックします。[Show only existing source roots] チェックボックスを無効にすることが必要な場合があります。f61561feb28a6445.png
  4. [Refactor] ボタンをクリックしてファイルを移動します。

CartItem および CartItemWithFruittie モデルを移動する

ファイル androidApp/.../model/CartItem.kt の場合は、次の手順に沿って操作する必要があります。

  1. ファイルを開き、CartItem クラスを右クリックして、[Refactor > Move] を選択します。
  2. これにより、同じ [Move] ダイアログが開きますが、この場合は CartItemWithFruittie メンバーのチェックボックスもオンにします。
  3. a25022cce5cee5e0.png Fruittie.kt ファイルの場合と同様に、... アイコンを選択し、commonMain ソースセットを選択して続行します。

DAO と AppDatabase を移動する

次のファイルについても同じ手順を行います(3 つすべてを同時に選択できます)。

  • androidApp/.../database/FruittieDao.kt
  • androidApp/.../database/CartDao.kt
  • androidApp/.../database/AppDatabase.kt

共有 AppDatabase を更新してプラットフォーム間で動作するようにする

データベース クラスを :shared モジュールに移動したので、両方のプラットフォームで必要な実装を生成するようにクラスを調整する必要があります。

  1. /shared/src/commonMain/kotlin/com/example/fruitties/kmptutorial/android/database/AppDatabase.kt ファイルを開きます。
  2. RoomDatabaseConstructor の実装を次のように追加します。
import androidx.room.RoomDatabaseConstructor

// The Room compiler generates the `actual` implementations.
@Suppress("NO_ACTUAL_FOR_EXPECT")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
    override fun initialize(): AppDatabase
}
  1. AppDatabase クラスに @ConstructedBy(AppDatabaseConstructor::class) アノテーションを付けます。
import androidx.room.ConstructedBy

@Database(
    entities = [Fruittie::class, CartItem::class],
    version = 1,
)
// TODO Add this line
@ConstructedBy(AppDatabaseConstructor::class)
abstract class AppDatabase : RoomDatabase() {
...

410a3c0c656b6499.png

データベースの作成を :shared モジュールに移動する

次に、Android 固有の Room の設定を :androidApp モジュールから :shared モジュールに移動します。これは、次のステップで :androidApp モジュールから Room 依存関係を削除するために必要です。

  1. androidApp/.../di/DatabaseModule.kt ファイルを見つけます。
  2. providesAppDatabase 関数のコンテンツを選択して右クリックし、[Refactor] > [Extract Function to Scope] を選択します。da4d97319f9a0e8c.png
  3. メニューで DatabaseModule.kt を選択します。5e540a1eec6e3493.png これにより、コンテンツがグローバルな appDatabase 関数に移動します。Enter キーを押して関数名を確定します。e2fb113d66704a36.png
  4. private の可視性修飾子を削除して、関数を公開します。
  5. 関数を :shared モジュールに移動するには、右クリックして [Refactor] > [Move] を選択します。
  6. [Move] ダイアログで、[Destination directory] フィールドの横にある [...] アイコンを選択します。e2101005f2ef4747.png
  7. [Choose Destination Directory] ダイアログで、[shared >androidMain] ソースセットを選択し、[/shared/src/androidMain/] フォルダを選択して、[OK] をクリックします。73d244941c68dc85.png
  8. [To package] フィールドのサフィックスを .di から .database に変更します。ac5cf30d32871e2c.png
  9. [Refactor] をクリックします。

:androidApp から不要なコードをクリーンアップする

この時点で、Room データベースをマルチプラットフォーム モジュールに移動しました。:androidApp モジュールでは Room の依存関係は必要ないため、削除できます。

  1. :androidApp モジュールの build.gradle.kts ファイルを開きます。
  2. 次のスニペットのように、依存関係と構成を削除します。
plugins {
  // TODO Remove 
  alias(libs.plugins.room)
}

android {
  // TODO Remove
  ksp {
      arg("room.generateKotlin", "true")
  }

dependencies {
  // TODO Keep room-runtime
  implementation(libs.androidx.room.runtime)

  // TODO Remove
  implementation(libs.androidx.sqlite.bundled)
  ksp(libs.androidx.room.compiler)
}

// TODO Remove
room {
    schemaDirectory("$projectDir/schemas")
}
  1. プロジェクトを Gradle 同期します。

Android アプリをビルドして実行する

Fruitties Android アプリを実行して、アプリが正しく動作し、:shared モジュールのデータベースが使用されていることを確認します。以前にカートアイテムを追加していた場合は、Room データベースが :shared モジュールに移動された後も、同じアイテムが表示されます。

6. iOS で Room を使用する準備をする

iOS プラットフォーム用に Room データベースをさらに準備するには、次のステップで使用するサポート コードを :shared モジュールに設定する必要があります。

iOS アプリのデータベース作成を有効にする

まず、iOS 固有のデータベース ビルダーを追加します。

  1. iosMain ソースセットの :shared モジュールに、AppDatabase.ios.kt という名前の新しいファイルを追加します。dcb46ba560298865.png
  2. 次のヘルパー関数を追加します。これらの関数は、iOS アプリが Room データベースのインスタンスを取得するために使用します。
package com.example.fruitties.kmptutorial.shared

import androidx.room.Room
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import com.example.fruitties.kmptutorial.android.database.AppDatabase
import kotlinx.cinterop.BetaInteropApi
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.cinterop.ObjCObjectVar
import kotlinx.cinterop.alloc
import kotlinx.cinterop.memScoped
import kotlinx.cinterop.ptr
import kotlinx.cinterop.value
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSError
import platform.Foundation.NSFileManager
import platform.Foundation.NSUserDomainMask

fun getPersistentDatabase(): AppDatabase {
    val dbFilePath = documentDirectory() + "/" + "fruits.db"
    return Room.databaseBuilder<AppDatabase>(name = dbFilePath)
       .setDriver(BundledSQLiteDriver())
       .build()
}

@OptIn(ExperimentalForeignApi::class, BetaInteropApi::class)
private fun documentDirectory(): String {
    memScoped {
        val errorPtr = alloc<ObjCObjectVar<NSError?>>()
        val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
            directory = NSDocumentDirectory,
            inDomain = NSUserDomainMask,
            appropriateForURL = null,
            create = false,
            error = errorPtr.ptr,
        )
        if (documentDirectory != null) {
            return requireNotNull(documentDirectory.path) {
                """Couldn't determine the document directory.
                  URL $documentDirectory does not conform to RFC 1808.
               """.trimIndent()
            }
        } else {
            val error = errorPtr.value
            val localizedDescription = error?.localizedDescription ?: "Unknown error occurred"
            error("Couldn't determine document directory. Error: $localizedDescription")
        }
    }
}

Room エンティティに「Entity」接尾辞を追加する

Swift で Room エンティティのラッパーを追加するため、Room エンティティの名前はラッパーの名前と異なるようにすることをおすすめします。@ObjCName アノテーションを使用して Room エンティティに Entity という接尾辞を追加することで、このことを確認します。

:shared モジュールの Fruittie.kt ファイルを開き、Fruittie エンティティに @ObjCName アノテーションを追加します。このアノテーションは試験運用版であるため、ファイルに @OptIn(ExperimentalObjC::class) アノテーションを追加する必要がある場合があります。

Fruittie.kt

import kotlin.experimental.ExperimentalObjCName
import kotlin.native.ObjCName

@OptIn(ExperimentalObjCName::class)
@Serializable
@Entity(indices = [Index(value = ["id"], unique = true)])
@ObjCName("FruittieEntity")
data class Fruittie(
   ...
)

次に、CartItem.kt ファイルの CartItem エンティティについても同様にします。

CartItem.kt

import kotlin.experimental.ExperimentalObjCName
import kotlin.native.ObjCName

@OptIn(ExperimentalObjCName::class)
@ObjCName("CartItemEntity")
@Entity(
   foreignKeys = [
       ForeignKey(
           entity = Fruittie::class,
           parentColumns = ["id"],
           childColumns = ["id"],
           onDelete = ForeignKey.CASCADE,
       ),
   ],
)
data class CartItem(@PrimaryKey val id: Long, val count: Int = 1)

7. iOS アプリで Room を使用する

iOS アプリは、Core Data を使用する既存のアプリです。このアプリは単なるプロトタイプであるため、この Codelab では、データベース内の既存のデータの移行について心配する必要はありません。製品版アプリを KMP に移行する場合は、現在の Core Data データベースを読み取り、移行後の初回起動時にそれらのアイテムを Room データベースに挿入する関数を記述する必要があります。

Xcode プロジェクトを開く

/iosApp/ フォルダに移動し、関連するアプリで Fruitties.xcodeproj を開いて、Xcode で iOS プロジェクトを開きます。

54836291a243ebe9.png

Core Data エンティティ クラスを削除する

まず、Core Data エンティティ クラスを削除して、後で作成するエンティティ ラッパーのスペースを確保する必要があります。アプリケーションが Core Data に保存するデータの種類に応じて、Core Data エンティティを完全に削除するか、データ移行のために保持するかを判断します。このチュートリアルでは、既存のデータを移行する必要がないため、削除して構いません。

Xcode で:

  1. Project Navigator に移動します。
  2. リソース フォルダに移動します。
  3. Fruitties ファイルを開きます。
  4. 各エンティティをクリックして削除します。

7ad742d991d76b1c.png

これらの変更をコードで使用できるようにするには、プロジェクトをクリーンアップして再ビルドします。

これにより、次のエラーが発生してビルドが失敗しますが、これは想定どおりです。

e3e107bf0387eeab.png

エンティティ ラッパーの作成

次に、Fruittie エンティティと CartItem エンティティのエンティティ ラッパーを作成し、Room エンティティと Core Data エンティティの API の違いをスムーズに処理します。

これらのラッパーを使用すると、すぐに更新する必要があるコードの量を最小限に抑え、Core Data から Room への移行をスムーズに進めることができます。今後は、これらのラッパーを Room エンティティへの直接アクセスに置き換えることを目標とする必要があります。

現時点では、FruittieEntity クラスのラッパーを作成し、任意のプロパティを指定します。

FruittieEntity ラッパーを作成する

  1. Sources/Repository ディレクトリに新しい Swift ファイルを作成するために、ディレクトリ名を右クリックして [Template...] から [New File] を選択します。cce140b2fb3c2da8.png 6a0d4fa4292ddd4f.png
  2. 名前を Fruittie に変更し、テスト対象ではなく Fruitties のターゲットのみが選択されるようにします。827b9019b0a32352.png
  3. 新しい Fruittie ファイルに次のコードを追加します。
import sharedKit

struct Fruittie: Hashable {
   let entity: FruittieEntity

   var id: Int64 {
       entity.id
   }

   var name: String? {
       entity.name
   }

   var fullName: String? {
       entity.fullName
   }
}

Fruittie 構造体は FruittieEntity クラスをラップし、プロパティを省略可能にしてエンティティのプロパティを渡します。また、Fruittie 構造体を Hashable プロトコルに準拠させ、SwiftUI の ForEach ビューで使用できるようにします。

CartItemEntity ラッパーを作成する

次に、CartItemEntity クラスに同様のラッパーを作成します。

Sources/Repository ディレクトリに CartItem.swift という新しい Swift ファイルを作成します。

import sharedKit

struct CartItem: Hashable {
   let entity: CartItemWithFruittie

   let fruittie: Fruittie?

   var id: Int64 {
       entity.cartItem.id
   }

   var count: Int64 {
       Int64(entity.cartItem.count)
   }

   init(entity: CartItemWithFruittie) {
       self.entity = entity
       self.fruittie = Fruittie(entity: entity.fruittie)
   }
}

元の Core Data の CartItem クラスには Fruittie プロパティがあったため、CartItem 構造体にも Fruittie プロパティが含まれています。CartItem クラスには省略可能なプロパティはありませんが、Room エンティティの count プロパティの型は異なります。

リポジトリを更新する

エンティティ ラッパーが設定されたので、Core Data ではなく Room を使用するように DefaultCartRepositoryDefaultFruittieRepository を更新する必要があります。

DefaultCartRepository の更新

2 つのうち、よりシンプルな DefaultCartRepository クラスから始めましょう。

Sources/Repository ディレクトリの CartRepository.swift ファイルを開きます。

  1. まず、CoreData インポートを sharedKit に置き換えます。
import sharedKit
  1. 次に、NSManagedObjectContext プロパティを削除し、CartDao プロパティに置き換えます。
// Remove
private let managedObjectContext: NSManagedObjectContext

// Replace with
private let cartDao: any CartDao
  1. 新しい cartDao プロパティを初期化するように init コンストラクタを更新します。
init(cartDao: any CartDao) {
    self.cartDao = cartDao
}
  1. 次に、addToCart メソッドを更新します。このメソッドでは、Core Data から既存のカートアイテムを取得する必要がありましたが、Room の実装ではこの必要はありません。代わりに、新しいアイテムを挿入するか、既存のカートアイテムの数を増やします。
func addToCart(fruittie: Fruittie) async throws {
    try await cartDao.insertOrIncreaseCount(fruittie: fruittie.entity)
}
  1. 最後に、getCartItems() メソッドを更新します。このメソッドは、CartDaogetAll() メソッドを呼び出し、CartItemWithFruittie エンティティを CartItem ラッパーにマッピングします。
func getCartItems() -> AsyncStream<[CartItem]> {
    return cartDao.getAll().map { entities in
        entities.map(CartItem.init(entity:))
    }.eraseToStream()
}

DefaultFruittieRepository の更新

DefaultFruittieRepository クラスを移行するには、DefaultCartRepository の場合と同様の変更を適用します。

FruittieRepository ファイルを次のように更新します。

import ConcurrencyExtras
import sharedKit

protocol FruittieRepository {
    func getData() -> AsyncStream<[Fruittie]>
}

class DefaultFruittieRepository: FruittieRepository {
    private let fruittieDao: any FruittieDao
    private let api: FruittieApi

    init(fruittieDao: any FruittieDao, api: FruittieApi) {
        self.fruittieDao = fruittieDao
        self.api = api
    }

    func getData() -> AsyncStream<[Fruittie]> {
        let dao = fruittieDao
        Task {
            let isEmpty = try await dao.count() == 0
            if isEmpty {
                let response = try await api.getData(pageNumber: 0)
                let fruitties = response.feed.map {
                    FruittieEntity(
                        id: 0,
                        name: $0.name,
                        fullName: $0.fullName,
                        calories: ""
                    )
                }
                _ = try await dao.insert(fruitties: fruitties)
            }
        }
        return dao.getAll().map { entities in
            entities.map(Fruittie.init(entity:))
        }.eraseToStream()
    }
}

@FetchRequest プロパティ ラッパーを置き換える

また、SwiftUI ビューの @FetchRequest プロパティ ラッパーも置き換える必要があります。@FetchRequest プロパティ ラッパーは、Core Data からデータを取得して変更を監視するために使用されるため、Room エンティティでは使用できません。代わりに、UIModel を使用してリポジトリからデータにアクセスしましょう。

  1. Sources/UI/CartView.swift ファイルで CartView を開きます。
  2. 実装を次のように置き換えます。
import SwiftUI

struct CartView : View {
    @State
    private var expanded = false

    @ObservedObject
    private(set) var uiModel: ContentViewModel

    var body: some View {
        if (uiModel.cartItems.isEmpty) {
            Text("Cart is empty, add some items").padding()
        } else {
            HStack {
                Text("Cart has \(uiModel.cartItems.count) items (\(uiModel.cartItems.reduce(0) { $0 + $1.count }))")
                    .padding()

                Spacer()

                Button {
                    expanded.toggle()
                } label: {
                    if (expanded) {
                        Text("collapse")
                    } else {
                        Text("expand")
                    }
                }
                .padding()
            }
            if (expanded) {
                VStack {
                    ForEach(uiModel.cartItems, id: \.self) { item in
                        Text("\(item.fruittie!.name!): \(item.count)")
                    }
                }
            }
        }
    }
}

ContentView を更新する

Sources/View/ContentView.swift ファイルの ContentView を更新して、FruittieUIModelCartView に渡します。

CartView(uiModel: uiModel)

DataController の更新

iOS アプリの DataController クラスは、Core Data スタックの設定を担当します。Core Data から移行するため、代わりに Room データベースを初期化するように DataController を更新する必要があります。

  1. Sources/DatabaseDataController.swift ファイルを開きます。
  2. sharedKit インポートを追加します。
  3. CoreData インポートを削除します。
  4. DataController クラスで Room データベースをインスタンス化します。
  5. 最後に、DataController イニシャライザから loadPersistentStores メソッド呼び出しを削除します。

最終クラスは次のようになるはずです。

import Combine
import sharedKit

class DataController: ObservableObject {
    let database = getPersistentDatabase()
    init() {}
}

依存関係インジェクションの更新

iOS アプリの AppContainer クラスは、依存関係グラフの初期化を担当します。Core Data ではなく Room を使用するようにリポジトリを更新したので、Room DAO をリポジトリに渡すように AppContainer を更新する必要があります。

  1. Sources/DI フォルダの AppContainer.swift を開きます。
  2. sharedKit インポートを追加します。
  3. AppContainer クラスから managedObjectContext プロパティを削除します。
  4. DataController によって提供される AppDatabase インスタンスの Room DAO を渡して、DefaultFruittieRepositoryDefaultCartRepository の初期化を変更します。

完了すると、クラスは次のようになります。

import Combine
import Foundation
import sharedKit

class AppContainer: ObservableObject {
    let dataController: DataController
    let api: FruittieApi
    let fruittieRepository: FruittieRepository
    let cartRepository: CartRepository

    init() {
        dataController = DataController()
        api = FruittieNetworkApi(
            apiUrl: URL(
                string:
                    "https://android.github.io/kotlin-multiplatform-samples/fruitties-api"
            )!)
        fruittieRepository = DefaultFruittieRepository(
            fruittieDao: dataController.database.fruittieDao(),
            api: api
        )
        cartRepository = DefaultCartRepository(
                    cartDao: dataController.database.cartDao()
        )
    }
}

最後に、main.swiftFruittiesApp を更新して managedObjectContext を削除します。

struct FruittiesApp: App {
    @StateObject
    private var appContainer = AppContainer()

    var body: some Scene {
        WindowGroup {
            ContentView(appContainer: appContainer)
        }
    }
}

iOS アプリをビルドして実行する

最後に、⌘R を押してアプリをビルドして実行すると、Core Data から Room に移行されたデータベースでアプリが起動します。

5d2ae9438747f8f6.png

8. 完了

お疲れさまでした。Room KMP を使用して、スタンドアロンの Android アプリと iOS アプリが共有データレイヤに正常に移行されました。

参考までに、アプリのアーキテクチャを比較して、達成された成果をご確認ください。

移行前

Android

iOS

KMP 統合前の Android アプリケーションのアーキテクチャ図

KMP 統合前の iOS アプリケーションのアーキテクチャ図

移行後のアーキテクチャ

bcd8c29b00f67c19.png

詳細