Migrer des applications existantes vers Room KMP

1. Avant de commencer

Conditions préalables

Ce dont vous avez besoin

Objectifs de l'atelier

  • Comment partager une base de données Room entre une application Android et une application iOS

2. Configuration

Pour commencer, procédez comme suit :

  1. Clonez le dépôt GitHub à l'aide de la commande de terminal suivante:
$ git clone https://github.com/android/codelab-android-kmp.git

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP :

  1. Dans Android Studio, ouvrez le projet migrate-room, qui contient les branches suivantes:
  • main : contient le code de démarrage de ce projet, que vous allez modifier tout au long de l'atelier.
  • end : contient le code de solution pour cet atelier.

Nous vous recommandons de commencer avec la branche main et de suivre l'atelier de programmation étape par étape, à votre rythme.

  1. Si vous souhaitez voir le code de solution, exécutez la commande suivante :
$ git clone -b end https://github.com/android/codelab-android-kmp.git

Vous pouvez également télécharger le code de solution :

3. Comprendre l'application exemple

Ce tutoriel consiste en l'application exemple Fruitties, créée dans des frameworks natifs (Jetpack Compose sur Android, SwiftUi sur iOS).

L'application Fruitties offre deux fonctionnalités principales:

  • Une liste d'éléments Fruit, chacun avec un bouton permettant d'ajouter l'élément à un panier.
  • Un panier s'affiche en haut de l'écran. Il indique le nombre de fruits ajoutés et leur quantité.

4a7f262b015d7f78.png

Architecture des applications Android

L'application Android suit les Consignes officielles concernant l'architecture Android pour conserver une structure claire et modulaire.

Schéma de l'architecture de l'application Android avant l'intégration de KMP

Architecture de l'application iOS

Schéma de l'architecture de l'application iOS avant l'intégration de KMP

Module partagé KMP

Ce projet a déjà été configuré avec un module partagé pour KMP, mais il est actuellement vide. Si votre projet n'a pas encore été configuré avec un module partagé, commencez par l'atelier de programmation Premiers pas avec Kotlin Multiplatform.

4. Préparer la base de données Room pour l'intégration de KMP

Avant de déplacer le code de la base de données Room de l'application Android Fruitties vers le module shared, vous devez vous assurer que l'application est compatible avec les API Room de Kotlin Multiplatform (KMP). Cette section vous guide tout au long de ce processus.

L'une des principales mises à jour consiste à utiliser un pilote SQLite compatible avec Android et iOS. Pour prendre en charge la fonctionnalité de la base de données Room sur plusieurs plates-formes, vous pouvez utiliser BundledSQLiteDriver. Ce pilote regroupe SQLite directement dans l'application, ce qui le rend adapté à une utilisation multiplate-forme en Kotlin. Pour obtenir des conseils détaillés, consultez le guide de migration de Room KMP.

Mettre à jour les dépendances

Commencez par ajouter les dépendances room-runtime et sqlite-bundled au fichier 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" }

Ensuite, mettez à jour le build.gradle.kts du module :androidApp pour utiliser ces dépendances et supprimez l'utilisation de libs.androidx.room.ktx :

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

Synchronisez maintenant le projet dans Android Studio.

Modifier le module de base de données pour BundledSQLiteDriver

Modifiez ensuite la logique de création de la base de données dans l'application Android pour utiliser le BundledSQLiteDriver, ce qui la rend compatible avec le langage KMP tout en conservant sa fonctionnalité sur Android.

  1. Ouvrez le fichier DatabaseModule.kt qui se trouve sous androidApp/src/main/kotlin/com/example/fruitties/kmptutorial/android/di/DatabaseModule.kt.
  2. Mettez à jour la méthode providesAppDatabase comme dans l'extrait de code suivant :
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()
}

Compiler et exécuter l'application Android

Maintenant que vous avez remplacé le pilote SQLite natif par le pilote fourni, vérifions que l'application est compilée et que tout fonctionne correctement avant de migrer la base de données vers le module :shared.

5. Déplacer le code de la base de données vers le module :shared

Au cours de cette étape, nous allons transférer la configuration de la base de données Room de l'application Android vers le module :shared, ce qui permettra à la base de données d'être accessible à la fois sur Android et sur iOS.

Mettre à jour la configuration build.gradle.kts du module :shared

Commencez par mettre à jour la configuration build.gradle.kts du module :shared pour utiliser les dépendances multiplates-formes Room.

  1. Ajoutez les plug-ins KSP et Room:
plugins {
   ...
   // TODO add KSP + ROOM plugins
   alias(libs.plugins.ksp)
   alias(libs.plugins.room)
}
  1. Ajoutez les dépendances room-runtime et sqlite-bundled au bloc commonMain:
sourceSets {
    commonMain {
        // TODO Add KMP dependencies here
        implementation(libs.androidx.room.runtime)
        implementation(libs.androidx.sqlite.bundled)
    }
}
  1. Ajoutez une configuration KSP pour chaque plate-forme cible en ajoutant un nouveau bloc dependencies de premier niveau. Pour plus de simplicité, vous pouvez l'ajouter en bas du fichier :
// 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. Toujours au premier niveau, ajoutez un bloc pour définir l'emplacement du schéma Room:
// Should be its own top level block. For convenience, add at the bottom of the file
room {
   schemaDirectory("$projectDir/schemas")
}
  1. Gradle synchronise le projet

Déplacer le schéma Room vers le module :shared

Déplacez le répertoire androidApp/schemas vers le dossier racine du module :shared, à côté du dossier src/ :

De : e1ee37a3f3a10b35.png

À : ba3c9eb617828bac.png

Déplacer des DAO et des entités

Maintenant que vous avez ajouté les dépendances Gradle nécessaires au module partagé KMP, vous devez déplacer les DAO et les entités du module :androidApp vers le module :shared.

Pour ce faire, vous devrez déplacer les fichiers vers leurs emplacements respectifs dans l'ensemble de sources commonMain du module :shared.

Déplacer le modèle Fruittie

Vous pouvez utiliser la fonctionnalité Refactoriser → Déplacer pour changer de module sans interrompre les importations :

  1. Trouvez le fichier androidApp/src/main/kotlin/.../model/Fruittie.kt, effectuez un clic droit dessus, puis sélectionnez Refactoriser → Déplacer (ou appuyez sur la touche F6) : c893e12b8bf683ae.png
  2. Dans la boîte de dialogue Move (Déplacer), sélectionnez l'icône ... à côté du champ Destination directory (Répertoire de destination). 1d51c3a410e8f2c3.png
  3. Sélectionnez l'ensemble de sources commonMain dans la boîte de dialogue Choose Destination Directory (Sélectionner le répertoire de destination), puis cliquez sur OK. Vous devrez peut-être désactiver la case à cocher Show only existing source roots (Afficher uniquement les racines de source existantes). f61561feb28a6445.png
  4. Cliquez sur le bouton Refactor (Refactoriser) pour déplacer le fichier.

Déplacer les modèles CartItem et CartItemWithFruittie

Pour le fichier androidApp/.../model/CartItem.kt, procédez comme suit:

  1. Ouvrez le fichier, effectuez un clic droit sur la classe CartItem, puis sélectionnez Refactor > Move (Refactoriser > Déplacer).
  2. La même boîte de dialogue Move (Déplacer) s'ouvre, mais dans ce cas, vous cochez également la case du membre CartItemWithFruittie.
  3. a25022cce5cee5e0.png Pour continuer, sélectionnez l'icône ..., puis l'ensemble de sources commonMain, comme vous l'avez fait pour le fichier Fruittie.kt.

Déplacer les DAO et AppDatabase

Suivez la même procédure pour les fichiers suivants (vous pouvez les sélectionner tous les trois en même temps) :

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

Mettre à jour le AppDatabase partagé pour qu'il fonctionne sur plusieurs plates-formes

Maintenant que les classes de base de données ont été déplacées vers le module :shared, vous devez les ajuster pour générer les implémentations requises sur les deux plates-formes.

  1. Ouvrez le fichier /shared/src/commonMain/kotlin/com/example/fruitties/kmptutorial/android/database/AppDatabase.kt.
  2. Ajoutez l'implémentation suivante de 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. Annotez la classe AppDatabase avec @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

Déplacer la création de la base de données vers le module :shared

Ensuite, déplacez la configuration Room spécifique à Android du module :androidApp vers le module :shared. Cette étape est nécessaire, car à l'étape suivante, vous allez supprimer la dépendance Room du module :androidApp.

  1. Trouvez le fichier androidApp/.../di/DatabaseModule.kt.
  2. Sélectionnez le contenu de la fonction providesAppDatabase, effectuez un clic droit, puis sélectionnez Refactor > Extract Function to Scope (Refactoriser > Extraire la fonction dans le champ d'application) : da4d97319f9a0e8c.png
  3. Sélectionnez DatabaseModule.kt dans le menu. 5e540a1eec6e3493.png : le contenu est déplacé vers une fonction appDatabase globale. Appuyez sur Enter (Entrée) pour confirmer le nom de la fonction. e2fb113d66704a36.png
  4. Rendez la fonction publique en supprimant le modificateur de visibilité private.
  5. Déplacez la fonction dans le module :shared en effectuant un clic droit sur Refactor > Move (Refactoriser > Déplacer).
  6. Dans la boîte de dialogue Move (Déplacer), sélectionnez l'icône située à côté du champ Destination directory (Répertoire de destination). e2101005f2ef4747.png
  7. Dans la boîte de dialogue Choose Destination Directory (Sélectionner le répertoire de destination), sélectionnez l'ensemble de sources shared > androidMain, puis le dossier /shared/src/androidMain/. Cliquez ensuite sur OK. 73d244941c68dc85.png
  8. Remplacez le suffixe .di par .databaseac5cf30d32871e2c.png dans le champ To package.
  9. Cliquez sur Refactor (Refactoriser).

Nettoyer le code inutile de :androidApp

À ce stade, vous avez déplacé la base de données Room vers le module multiplate-forme. Aucune des dépendances Room n'est nécessaire dans le module :androidApp. Vous pouvez donc les supprimer.

  1. Ouvrez le fichier build.gradle.kts dans le module :androidApp.
  2. Supprimez les dépendances et la configuration comme dans l'extrait de code suivant :
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 synchronise le projet.

Compiler et exécuter l'application Android

Exécutez l'application Android Fruitties pour vous assurer qu'elle fonctionne correctement et qu'elle utilise désormais la base de données du module :shared. Si vous aviez déjà ajouté des articles au panier, vous devriez voir les mêmes articles à ce stade, même si la base de données Room se trouve désormais dans le module :shared.

6. Préparer Room pour une utilisation sur iOS

Pour préparer la base de données Room pour la plate-forme iOS, vous devez configurer un code de compatibilité dans le module :shared pour l'utiliser à l'étape suivante.

Permettre la création de bases de données pour l'application iOS

La première chose à faire est d'ajouter un compilateur de base de données spécifique à iOS.

  1. Ajoutez un fichier dans le module :shared de l'ensemble de sources iosMain nommé AppDatabase.ios.kt : dcb46ba560298865.png
  2. Ajoutez les fonctions d'assistance suivantes. Ces fonctions seront utilisées par l'application iOS pour obtenir une instance de la base de données 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")
        }
    }
}

Ajout du suffixe "Entity" aux entités Room

Comme vous ajoutez des wrappers pour les entités Room dans Swift, il est préférable que les noms des entités Room soient différents de ceux des wrappers. Pour nous en assurer, nous allons ajouter le suffixe Entity aux entités Room à l'aide de l'annotation @ObjCName.

Ouvrez le fichier Fruittie.kt dans le module :shared et ajoutez l'annotation @ObjCName à l'entité Fruittie. Comme cette annotation est expérimentale, vous devrez peut-être ajouter l'annotation @OptIn(ExperimentalObjC::class) au fichier.

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(
   ...
)

Ensuite, procédez de la même manière pour l'entité CartItem dans le fichier CartItem.kt.

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. Utiliser Room dans l'application iOS

L'application iOS est une application préexistante qui utilise Core Data. Dans cet atelier de programmation, vous n'aurez pas à vous soucier de la migration des données existantes dans la base de données, car cette application n'est qu'un prototype. Si vous migrez une application de production vers KMP, vous devrez écrire des fonctions pour lire la base de données Core Data actuelle et insérer ces éléments dans la base de données Room lors du premier lancement après la migration.

Ouvrir le projet Xcode

Ouvrez le projet iOS dans Xcode en accédant au dossier /iosApp/ et en ouvrant Fruitties.xcodeproj dans l'application associée.

54836291a243ebe9.png

Supprimer les classes d'entités Core Data

Tout d'abord, vous devez supprimer les classes d'entités Core Data pour laisser de la place aux wrappers d'entités que vous créerez plus tard. Selon le type de données stockées par votre application dans Core Data, vous pouvez supprimer complètement les entités Core Data ou les conserver à des fins de migration des données. Pour ce tutoriel, vous pouvez simplement les supprimer, car vous n'aurez pas besoin de migrer les données existantes.

Dans Xcode :

  1. Accédez au navigateur de projet.
  2. Accédez au dossier "Resources".
  3. Ouvrez le fichier Fruitties.
  4. Cliquez sur chaque entité et supprimez-les.

7ad742d991d76b1c.png

Pour rendre ces modifications disponibles dans le code, nettoyez et recompilez le projet.

La compilation échoue et les erreurs suivantes s'affichent, ce qui est normal.

e3e107bf0387eeab.png

Créer des wrappers d'entités

Nous allons ensuite créer des wrappers d'entités pour les entités Fruittie et CartItem afin de gérer facilement les différences d'API entre les entités Room et Core Data.

Ces wrappers nous aideront à passer de Core Data à Room en réduisant la quantité de code à mettre à jour immédiatement. À l'avenir, vous devrez remplacer ces wrappers par un accès direct aux entités Room.

Pour l'instant, nous allons créer un wrapper pour la classe FruittieEntity, en lui attribuant des propriétés facultatives.

Créer un wrapper FruittieEntity

  1. Créez un nouveau fichier Swift dans le répertoire Sources/Repository en effectuant un clic droit sur le nom du répertoire, puis en sélectionnant New File from Template… (Nouveau fichier à partir du modèle…) cce140b2fb3c2da8.png 6a0d4fa4292ddd4f.png
  2. Nommez-le Fruittie et assurez-vous que seule la cible Fruitties est sélectionnée, et non la cible de test. 827b9019b0a32352.png
  3. Ajoutez le code suivant au nouveau fichier Fruittie:
import sharedKit

struct Fruittie: Hashable {
   let entity: FruittieEntity

   var id: Int64 {
       entity.id
   }

   var name: String? {
       entity.name
   }

   var fullName: String? {
       entity.fullName
   }
}

Le struct Fruittie encapsule la classe FruittieEntity, ce qui rend les propriétés facultatives et les transmet à aux propriétés de l'entité. De plus, nous rendons le struct Fruittie conforme au protocole Hashable afin qu'il puisse être utilisé dans l'affichage ForEach de SwiftUI.

Créer un wrapper CartItemEntity

Créez ensuite un wrapper similaire pour la classe CartItemEntity.

Créez un fichier Swift nommé CartItem.swift dans le répertoire Sources/Repository.

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)
   }
}

Étant donné que la classe Core Data CartItem d'origine comportait une propriété Fruittie, nous avons également inclus une propriété Fruittie dans le struct CartItem. Bien que la classe CartItem ne comporte aucune propriété facultative, la propriété count a un type différent dans l'entité Room.

Mettre à jour les dépôts

Maintenant que les wrappers d'entité sont en place, vous devez mettre à jour DefaultCartRepository et DefaultFruittieRepository pour utiliser Room au lieu de Core Data.

Mettre à jour DefaultCartRepository

Commençons par la classe DefaultCartRepository, qui est la plus simple des deux.

Ouvrez le fichier CartRepository.swift dans le répertoire Sources/Repository.

  1. Commencez par remplacer l'importation CoreData par sharedKit :
import sharedKit
  1. Supprimez ensuite la propriété NSManagedObjectContext et remplacez-la par une propriété CartDao :
// Remove
private let managedObjectContext: NSManagedObjectContext

// Replace with
private let cartDao: any CartDao
  1. Mettez à jour le constructeur init pour initialiser la nouvelle propriété cartDao :
init(cartDao: any CartDao) {
    self.cartDao = cartDao
}
  1. Mettez ensuite à jour la méthode addToCart. Cette méthode exigeait d'extraire les articles de panier existants de Core Data, mais notre implémentation Room ne nécessite pas cette opération. Au lieu de cela, un nouvel article est inséré ou le nombre d'articles d'un panier existant est incrémenté.
func addToCart(fruittie: Fruittie) async throws {
    try await cartDao.insertOrIncreaseCount(fruittie: fruittie.entity)
}
  1. Enfin, mettez à jour la méthode getCartItems(). Cette méthode appelle la méthode getAll() sur CartDao et mappe les entités CartItemWithFruittie à nos wrappers CartItem.
func getCartItems() -> AsyncStream<[CartItem]> {
    return cartDao.getAll().map { entities in
        entities.map(CartItem.init(entity:))
    }.eraseToStream()
}

Mettre à jour DefaultFruittieRepository

Pour migrer la classe DefaultFruittieRepository, nous appliquons des modifications similaires à celles que nous avons appliquées pour DefaultCartRepository.

Mettez à jour le fichier FruittieRepository comme suit:

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()
    }
}

Remplacer les wrappers de propriété @FetchRequest

Nous devons également remplacer les wrappers de propriété @FetchRequest dans les affichages SwiftUI. Le wrapper de propriété @FetchRequest permet d'extraire des données à partir de Core Data et d'observer les modifications. Nous ne pouvons donc pas l'utiliser avec des entités Room. Utilisons plutôt UIModel pour accéder aux données des dépôts.

  1. Ouvrez CartView dans le fichier Sources/UI/CartView.swift.
  2. Remplacez l'implémentation par le code suivant:
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)")
                    }
                }
            }
        }
    }
}

Mettre à jour ContentView

Mettez à jour ContentView dans le fichier Sources/View/ContentView.swift pour transmettre FruittieUIModel à CartView.

CartView(uiModel: uiModel)

Mettre à jour DataController

La classe DataController de l'application iOS est chargée de configurer la pile Core Data. Étant donné que nous abandonnons Core Data, nous devons mettre à jour DataController pour initialiser la base de données Room à la place.

  1. Ouvrez le fichier DataController.swift dans Sources/Database.
  2. Ajoutez l'importation sharedKit.
  3. Supprimez l'importation CoreData.
  4. Instanciez la base de données Room dans la classe DataController.
  5. Enfin, supprimez l'appel de méthode loadPersistentStores de l'initialiseur DataController.

La classe finale devrait se présenter comme suit :

import Combine
import sharedKit

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

Mettre à jour l'injection de dépendances

La classe AppContainer de l'application iOS est chargée d'initialiser le graphique de dépendance. Étant donné que nous avons mis à jour les dépôts pour utiliser Room au lieu de Core Data, nous devons mettre à jour AppContainer pour transmettre les DAO Room aux dépôts.

  1. Ouvrez AppContainer.swift dans le dossier Sources/DI.
  2. Ajoutez l'importation sharedKit.
  3. Supprimez la propriété managedObjectContext de la classe AppContainer.
  4. Modifiez les initialisations DefaultFruittieRepository et DefaultCartRepository en transmettant des DAO Room à partir de l'instance AppDatabase fournie par DataController.

Lorsque vous avez terminé, la classe se présente comme suit :

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()
        )
    }
}

Enfin, mettez à jour FruittiesApp dans main.swift pour supprimer managedObjectContext :

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

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

Compiler et exécuter l'application iOS

Enfin, une fois que vous avez compilé et exécuté l'application en appuyant sur ⌘R, l'application devrait démarrer avec la base de données migrée de Core Data vers Room.

5d2ae9438747f8f6.png

8. Félicitations

Félicitations ! Vous avez migré des applications Android et iOS autonomes vers une couche de données partagée à l'aide de Room KMP.

Pour référence, voici la comparaison des architectures d'applications pour voir ce qui a été réalisé :

Avant

Android

iOS

Schéma de l&#39;architecture de l&#39;application Android avant l&#39;intégration de KMP

Schéma de l&#39;architecture de l&#39;application iOS avant l&#39;intégration de KMP

Architecture après la migration

bcd8c29b00f67c19.png

En savoir plus