Migra apps existentes a Room en KMP

1. Antes de comenzar

Requisitos previos

Requisitos

Qué aprenderás

  • Cómo compartir una base de datos de Room entre una app para Android y una para iOS

2. Prepárate

Para comenzar, sigue estos pasos:

  1. Clona el repositorio de GitHub con el siguiente comando de la terminal:
$ git clone https://github.com/android/codelab-android-kmp.git

También tienes la opción de descargar el repositorio como archivo ZIP:

  1. En Android Studio, abre el proyecto migrate-room, que contiene las siguientes ramas:
  • main: contiene el código de partida para este proyecto, en el que realizarás cambios para completar el codelab.
  • end: contiene el código de solución para este codelab.

Te recomendamos que comiences con la rama main y sigas el codelab paso a paso a tu propio ritmo.

  1. Si quieres ver el código de solución, ejecuta este comando:
$ git clone -b end https://github.com/android/codelab-android-kmp.git

También puedes descargar el código de solución:

3. Comprende la app de ejemplo

Este instructivo consta de la aplicación de ejemplo Fruitties compilada en frameworks nativos (Jetpack Compose en Android y SwiftUI en iOS).

La app de Fruitties ofrece dos funciones principales:

  • Una lista de elementos Fruta, cada uno con un botón para agregar el elemento a un carrito.
  • Un carrito que se muestra en la parte superior, en el que se indica cuántas frutas se agregaron y sus cantidades.

4a7f262b015d7f78.png

Arquitectura de apps para Android

La app para Android sigue los lineamientos oficiales de arquitectura de Android para mantener una estructura clara y modular.

Diagrama de arquitectura de la aplicación para Android antes de la integración de KMP

Arquitectura de la app para iOS

Diagrama de arquitectura de la aplicación para iOS antes de la integración de KMP

Módulo compartido de KMP

Este proyecto ya se configuró con un módulo compartido para KMP, aunque actualmente está vacío. Si aún no configuraste tu proyecto con un módulo compartido, comienza con el codelab Introducción a Kotlin Multiplataforma.

4. Prepara la base de datos de Room para la integración de KMP

Antes de mover el código de la base de datos de Room de la app para Android Fruitties al módulo shared, debes asegurarte de que la app sea compatible con las APIs de Room de Kotlin Multiplataforma (KMP). En esta sección, se explica ese proceso.

Una actualización clave es usar un controlador SQLite compatible con Android y iOS. Para admitir la funcionalidad de la base de datos de Room en varias plataformas, puedes usar BundledSQLiteDriver. Este controlador empaqueta SQLite directamente en la aplicación, lo que lo hace adecuado para el uso multiplataforma en Kotlin. Para obtener ayuda detallada, consulta la guía de migración de Room en KMP.

Actualiza las dependencias

Primero, agrega las dependencias room-runtime y sqlite-bundled al archivo 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" }

Luego, actualiza el objeto build.gradle.kts del módulo :androidApp para usar estas dependencias y quita el uso de libs.androidx.room.ktx:

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

Ahora, sincroniza el proyecto en Android Studio.

Modifica el módulo de base de datos para BundledSQLiteDriver

A continuación, modifica la lógica de creación de la base de datos en la app para Android para usar BundledSQLiteDriver, lo que la hará compatible con KMP y, al mismo tiempo, mantendrá la funcionalidad en Android.

  1. Abre el archivo DatabaseModule.kt ubicado en androidApp/src/main/kotlin/com/example/fruitties/kmptutorial/android/di/DatabaseModule.kt.
  2. Actualiza el método providesAppDatabase como en el siguiente fragmento:
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()
}

Compila y ejecuta la app para Android

Ahora que cambiaste el controlador SQLite nativo al incluido en el paquete, verifiquemos que la app se compile y todo funcione correctamente antes de migrar la base de datos al módulo :shared.

5. Mueve el código de la base de datos al módulo :shared

En este paso, transferiremos la configuración de la base de datos de Room de la app para Android al módulo :shared, lo que permitirá que Android y iOS puedan acceder a la base de datos.

Actualiza la configuración de build.gradle.kts del módulo :shared.

Comienza por actualizar el build.gradle.kts del módulo :shared para usar las dependencias multiplataforma de Room.

  1. Agrega los complementos KSP y Room:
plugins {
   ...
   // TODO add KSP + ROOM plugins
   alias(libs.plugins.ksp)
   alias(libs.plugins.room)
}
  1. Agrega las dependencias room-runtime y sqlite-bundled al bloque commonMain:
sourceSets {
    commonMain {
        // TODO Add KMP dependencies here
        implementation(libs.androidx.room.runtime)
        implementation(libs.androidx.sqlite.bundled)
    }
}
  1. Agrega un nuevo bloque dependencies de nivel superior para agregar la configuración de KSP en cada objetivo de plataforma. Para facilitar el proceso, puedes agregarlo al final del archivo:
// 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. También en el nivel superior, agrega un nuevo bloque para establecer la ubicación del esquema Room:
// Should be its own top level block. For convenience, add at the bottom of the file
room {
   schemaDirectory("$projectDir/schemas")
}
  1. Sincroniza el proyecto con Gradle

Mueve el esquema de Room al módulo :shared

Mueve el directorio androidApp/schemas a la carpeta raíz del módulo :shared junto a la carpeta src/:

De: e1ee37a3f3a10b35.png

Para: ba3c9eb617828bac.png

Mueve DAO y entidades

Ahora que agregaste las dependencias de Gradle necesarias al módulo compartido de KMP, debes mover las DAO y las entidades del módulo :androidApp al módulo :shared.

Esto implicará mover archivos a sus ubicaciones respectivas en el conjunto de orígenes commonMain del módulo :shared.

Mueve el modelo Fruittie

Puedes aprovechar la funcionalidad Refactor → Move para cambiar de módulo sin interrumpir las importaciones:

  1. Busca el archivo androidApp/src/main/kotlin/.../model/Fruittie.kt, haz clic con el botón derecho en él y selecciona Refactor → Move (o la tecla F6):c893e12b8bf683ae.png
  2. En el diálogo Move, selecciona el ícono ... junto al campo Destination directory. 1d51c3a410e8f2c3.png
  3. Selecciona el conjunto de orígenes commonMain en el diálogo Choose Destination Directory y haz clic en Ok. Es posible que debas inhabilitar la casilla de verificación Show only existing source roots. f61561feb28a6445.png
  4. Haz clic en el botón Refactor para mover el archivo.

Mueve los modelos CartItem y CartItemWithFruittie

Para el archivo androidApp/.../model/CartItem.kt, debes seguir estos pasos:

  1. Abre el archivo, haz clic con el botón derecho en la clase CartItem y selecciona Refactor > Move.
  2. Se abrirá el mismo diálogo Move. Sin embargo, en este caso, también debes marcar la casilla del miembro CartItemWithFruittie.
  3. a25022cce5cee5e0.png Para continuar, selecciona el ícono ... y el conjunto de orígenes commonMain, tal como lo hiciste con el archivo Fruittie.kt.

Mueve DAO y AppDatabase

Repite los mismos pasos para los siguientes archivos (puedes seleccionar los tres al mismo tiempo):

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

Actualiza el AppDatabase compartido para que funcione en varias plataformas

Ahora que moviste las clases de la base de datos al módulo :shared, debes ajustarlas para generar las implementaciones requeridas en ambas plataformas.

  1. Abre el archivo /shared/src/commonMain/kotlin/com/example/fruitties/kmptutorial/android/database/AppDatabase.kt.
  2. Agrega la siguiente implementación 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. Anota la clase AppDatabase con @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

Mueve la creación de bases de datos al módulo :shared

Luego, moverás la configuración de Room específica para Android del módulo :androidApp al módulo :shared. Es necesario realizar esto porque, en el siguiente paso, quitarás la dependencia de Room del módulo :androidApp.

  1. Ubica el archivo androidApp/.../di/DatabaseModule.kt.
  2. Selecciona el contenido de la función providesAppDatabase, haz clic con el botón derecho y selecciona Refactor > Extract Function to Scope: da4d97319f9a0e8c.png
  3. Selecciona DatabaseModule.kt en el menú. 5e540a1eec6e3493.png Esto mueve el contenido a una función appDatabase global. Presiona Intro para confirmar el nombre de la función. e2fb113d66704a36.png
  4. Para que la función sea pública, quita el modificador de visibilidad private.
  5. Para mover la función al módulo :shared, haz clic con el botón derecho en Refactor > Move.
  6. En el cuadro de diálogo Move, selecciona el ícono junto al campo Destination directory. e2101005f2ef4747.png
  7. En el cuadro de diálogo Choose Destination Directory, selecciona el conjunto de orígenes shared >androidMain y la carpeta /shared/src/androidMain/. Luego, haz clic en OK.73d244941c68dc85.png
  8. Cambia el sufijo en el campo To package de .di a .databaseac5cf30d32871e2c.png.
  9. Haz clic en Refactor.

Limpia el código innecesario de :androidApp

En este punto, moviste la base de datos de Room al módulo multiplataforma y no se necesita ninguna de las dependencias de Room en el módulo :androidApp, por lo que puedes quitarlas.

  1. Abre el archivo build.gradle.kts en el módulo :androidApp.
  2. Quita las dependencias y la configuración como en el siguiente fragmento:
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. Sincroniza el proyecto con Gradle.

Compila y ejecuta la app para Android

Ejecuta la app para Android Fruitties para asegurarte de que se funciona correctamente y ahora use la base de datos del módulo :shared. Si agregaste artículos al carrito antes, deberías ver los mismos artículos en este punto, aunque la base de datos de Room ahora se encuentra en el módulo :shared.

6. Prepara Room para su uso en iOS

Para preparar aún más la base de datos de Room para la plataforma iOS, debes configurar un código de compatibilidad en el módulo :shared para usarlo en el siguiente paso.

Habilita la creación de bases de datos para la app para iOS

Lo primero que debes hacer es agregar un compilador de bases de datos específico para iOS.

  1. Agrega un archivo nuevo en el módulo :shared del conjunto de orígenes iosMain llamado AppDatabase.ios.kt: dcb46ba560298865.png
  2. Agrega las siguientes funciones auxiliares. La app para iOS usará estas funciones para obtener una instancia de la base de datos de 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")
        }
    }
}

Agrega el sufijo "Entity" a las entidades de Room

Como agregas wrappers para las entidades de Room en Swift, es mejor que los nombres de las entidades de Room difieran de los nombres de los wrappers. Para asegurarnos de que sea así, agregaremos el sufijo Entity a las entidades de Room con la anotación @ObjCName.

Abre el archivo Fruittie.kt en el módulo :shared y agrega la anotación @ObjCName a la entidad Fruittie. Como esta anotación es experimental, es posible que debas agregar la anotación @OptIn(ExperimentalObjC::class) al archivo.

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

Luego, haz lo mismo con la entidad CartItem en el archivo 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. Usa Room en la app para iOS

La app para iOS es una app preexistente que usa Core Data. En este codelab, no tendrás que preocuparte por migrar los datos existentes en la base de datos, ya que esta app es solo un prototipo. Si migras una app de producción a KMP, tendrás que escribir funciones para leer la base de datos actual de Core Data y, luego, insertar esos elementos en la base de datos de Room en el primer inicio después de la migración.

Abre el proyecto de Xcode

Para abrir el proyecto de iOS en Xcode, navega a la carpeta /iosApp/ y abre Fruitties.xcodeproj en la app asociada.

54836291a243ebe9.png

Quita las clases de entidades de Core Data

Primero, debes quitar las clases de entidades de Core Data para dejar espacio para los wrappers de entidades que crearás más adelante. Según el tipo de datos que almacene tu aplicación en Core Data, puedes quitar las entidades de Core Data por completo o conservarlas para la migración de datos. Para este instructivo, puedes quitarlas, ya que no necesitarás migrar ningún dato existente.

En Xcode:

  1. Ve a Project Navigator.
  2. Ve a la carpeta Resources.
  3. Abre el archivo Fruitties.
  4. Haz clic en cada entidad y bórralas.

7ad742d991d76b1c.png

Para que estos cambios estén disponibles en el código, limpia y vuelve a compilar el proyecto.

Esto hará que la compilación falle con los siguientes errores, lo cual es esperable.

e3e107bf0387eeab.png

Cómo crear wrappers de entidades

A continuación, crearemos wrappers de entidades para las entidades Fruittie y CartItem para controlar de manera fluida las diferencias de API entre las entidades de Room y Core Data.

Estos wrappers nos ayudarán a realizar la transición de Core Data a Room, ya que minimizan la cantidad de código que se debe actualizar de inmediato. En el futuro, debes reemplazar estos wrappers por accesos directos a las entidades de Room.

Por ahora, en su lugar, crearemos un wrapper para la clase FruittieEntity y le daremos propiedades opcionales.

Crea un wrapper FruittieEntity

  1. Para crear un archivo Swift nuevo en el directorio Sources/Repository, haz clic con el botón derecho en el nombre del directorio y selecciona New File from Template… cce140b2fb3c2da8.png 6a0d4fa4292ddd4f.png
  2. Nómbralo Fruittie y asegúrate de que solo se seleccione el objetivo de Fruitties, no el objetivo de prueba. 827b9019b0a32352.png
  3. Agrega el siguiente código al nuevo archivo de Fruittie:
import sharedKit

struct Fruittie: Hashable {
   let entity: FruittieEntity

   var id: Int64 {
       entity.id
   }

   var name: String? {
       entity.name
   }

   var fullName: String? {
       entity.fullName
   }
}

El struct Fruittie une la clase FruittieEntity, lo que hace que las propiedades sean opcionales y pase a través de las propiedades de la entidad. Además, hacemos que el struct Fruittie cumpla con el protocolo Hashable, de modo que se pueda usar en la vista ForEach de SwiftUI.

Crea un wrapper CartItemEntity

A continuación, crea un wrapper similar para la clase CartItemEntity.

Crea un archivo Swift nuevo llamado CartItem.swift en el directorio 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)
   }
}

Dado que la clase CartItem original de Core Data tenía una propiedad Fruittie, también incluimos una propiedad Fruittie en el struct CartItem. Si bien la clase CartItem no tiene ninguna propiedad opcional, la propiedad count tiene un tipo diferente en la entidad Room.

Actualiza los repositorios

Ahora que los wrappers de entidades están en su lugar, debes actualizar DefaultCartRepository y DefaultFruittieRepository para usar Room en lugar de Core Data.

Actualiza DefaultCartRepository

Comencemos con la clase DefaultCartRepository, ya que es la más simple de las dos.

Abre el archivo CartRepository.swift en el directorio Sources/Repository.

  1. Primero, reemplaza la importación de CoreData por sharedKit:
import sharedKit
  1. Luego, quita la propiedad NSManagedObjectContext y reemplázala por una propiedad CartDao:
// Remove
private let managedObjectContext: NSManagedObjectContext

// Replace with
private let cartDao: any CartDao
  1. Actualiza el constructor de init para inicializar la nueva propiedad cartDao:
init(cartDao: any CartDao) {
    self.cartDao = cartDao
}
  1. A continuación, actualiza el método addToCart. Este método necesitaba extraer elementos del carrito existentes de Core Data, pero nuestra implementación de Room no lo requiere. En su lugar, insertará un elemento nuevo o incrementará el recuento de un elemento del carrito existente.
func addToCart(fruittie: Fruittie) async throws {
    try await cartDao.insertOrIncreaseCount(fruittie: fruittie.entity)
}
  1. Por último, actualiza el método getCartItems(). Este método llamará al método getAll() en CartDao y asignará las entidades CartItemWithFruittie a nuestros wrappers CartItem.
func getCartItems() -> AsyncStream<[CartItem]> {
    return cartDao.getAll().map { entities in
        entities.map(CartItem.init(entity:))
    }.eraseToStream()
}

Actualiza DefaultFruittieRepository

Para migrar la clase DefaultFruittieRepository, aplicamos cambios similares a los que hicimos para DefaultCartRepository.

Actualiza el archivo FruittieRepository a lo siguiente:

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

Reemplaza los wrappers de propiedades @FetchRequest

También debemos reemplazar los wrappers de propiedades @FetchRequest en las vistas de SwiftUI. El wrapper de propiedades @FetchRequest se usa para recuperar datos de Core Data y observar cambios, por lo que no podemos usarlo con entidades de Room. En su lugar, usemos UIModel para acceder a los datos de los repositorios.

  1. Abre el CartView en el archivo Sources/UI/CartView.swift.
  2. Reemplaza la implementación por lo siguiente:
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)")
                    }
                }
            }
        }
    }
}

Actualiza ContentView

Actualiza ContentView en el archivo Sources/View/ContentView.swift para pasar FruittieUIModel a CartView.

CartView(uiModel: uiModel)

Cómo actualizar DataController

La clase DataController en la aplicación para iOS es responsable de configurar la pila de Core Data. Dado que nos alejaremos de Core Data, debemos actualizar el DataController para inicializar la base de datos de Room.

  1. Abre el archivo DataController.swift en Sources/Database.
  2. Agrega la importación de sharedKit.
  3. Quita la importación de CoreData.
  4. Crea una instancia de la base de datos de Room en la clase DataController.
  5. Por último, quita la llamada al método loadPersistentStores del inicializador DataController.

La clase actualizada debería verse de la siguiente manera:

import Combine
import sharedKit

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

Actualiza la inserción de dependencias

La clase AppContainer en la aplicación para iOS es responsable de inicializar el gráfico de dependencias. Dado que actualizamos los repositorios para usar Room en lugar de Core Data, debemos actualizar AppContainer para pasar los DAO de Room a los repositorios.

  1. Abre AppContainer.swift en la carpeta Sources/DI.
  2. Agrega la importación de sharedKit.
  3. Quita la propiedad managedObjectContext de la clase AppContainer.
  4. Para cambiar las inicializaciones de DefaultFruittieRepository y DefaultCartRepository, pasa los DAO de Room de la instancia AppDatabase que proporciona DataController.

Cuando termines, la clase se verá de la siguiente manera:

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

Por último, actualiza FruittiesApp en main.swift para quitar managedObjectContext:

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

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

Compila y ejecuta la app para iOS

Por último, una vez que compiles y ejecutes la app presionando ⌘R, esta debería iniciarse con la base de datos migrada de Core Data a Room.

5d2ae9438747f8f6.png

8. Felicitaciones

¡Felicitaciones! Migraste correctamente apps independientes para iOS y Android a una capa de datos compartida con Room en KMP.

A modo de referencia, esta es la comparación de las arquitecturas de la app para ver lo que se logró:

Antes

Android

iOS

Diagrama de arquitectura de la aplicación para Android antes de la integración de KMP

Diagrama de arquitectura de la aplicación para iOS antes de la integración de KMP

Arquitectura posterior a la migración

bcd8c29b00f67c19.png

Más información