Migrar apps atuais para o KMP do Room

1. Antes de começar

Pré-requisitos

O que você precisa

O que você vai aprender

  • Como compartilhar um banco de dados do Room entre um app Android e um app iOS.

2. Começar a configuração

Para começar, siga estas etapas:

  1. Clone o repositório do GitHub com o seguinte comando do terminal:
$ git clone https://github.com/android/codelab-android-kmp.git

Se preferir, faça o download do repositório como um arquivo ZIP:

  1. No Android Studio, abra o projeto migrate-room, que contém as ramificações abaixo:
  • main: contém o código inicial do projeto, em que você vai fazer mudanças para concluir o codelab.
  • end: contém o código da solução do codelab.

É recomendado começar pela ramificação main e seguir cada etapa do codelab no seu próprio ritmo.

  1. Para conferir o código da solução, execute este comando:
$ git clone -b end https://github.com/android/codelab-android-kmp.git

Você também pode baixar o código da solução:

3. Entender o app de exemplo

Este tutorial consiste no aplicativo de exemplo Fruitties criado em frameworks nativos (Jetpack Compose no Android, SwiftUi no iOS).

O app Fruitties oferece dois recursos principais:

  • Uma lista de itens de frutas, cada um com um botão para adicionar o item a um carrinho.
  • Um carrinho que aparece na parte de cima, mostrando quantas frutas foram adicionadas e as quantidades.

4a7f262b015d7f78.png

Arquitetura do app Android

O app Android segue as diretrizes oficiais de arquitetura do Android para manter uma estrutura clara e modular.

Diagrama de arquitetura do app Android antes da integração do KMP

Arquitetura do app iOS

Diagrama de arquitetura do app iOS antes da integração do KMP

Módulo compartilhado do KMP

Este projeto já foi configurado com um módulo compartilhado para o KMP, embora esteja vazio no momento. Se o projeto ainda não tiver um módulo compartilhado, comece com o codelab Começar a usar o Kotlin Multiplatform.

4. Preparar o banco de dados do Room para a integração do KMP

Antes de mover o código do banco de dados do Room do app Android Fruitties para o módulo shared, verifique se ele é compatível com as APIs Room para Kotlin Multiplatform (KMP). Esta seção explica esse processo.

Uma das principais atualizações é o uso de um driver SQLite compatível com Android e iOS. Para oferecer suporte à funcionalidade do banco de dados do Room em várias plataformas, use BundledSQLiteDriver. Esse driver agrupa o SQLite diretamente no aplicativo, tornando-o adequado para uso em várias plataformas no Kotlin. Para receber orientações detalhadas, consulte o guia de migração do Room KMP.

Atualizar dependências

Primeiro, adicione as dependências room-runtime e sqlite-bundled ao arquivo 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" }

Em seguida, atualize o build.gradle.kts do módulo :androidApp para usar essas dependências e remova o uso de libs.androidx.room.ktx:

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

Agora, sincronize o projeto no Android Studio.

Modificar o módulo de banco de dados do BundledSQLiteDriver

Em seguida, modifique a lógica de criação de banco de dados no app Android para usar o BundledSQLiteDriver, tornando-o compatível com o KMP e mantendo a funcionalidade no Android.

  1. Abra o arquivo DatabaseModule.kt localizado em androidApp/src/main/kotlin/com/example/fruitties/kmptutorial/android/di/DatabaseModule.kt.
  2. Atualize o método providesAppDatabase conforme o snippet abaixo:
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()
}

Criar e executar o app Android

Agora que você mudou o driver SQLite nativo para o agrupado, vamos verificar os builds do app e se tudo funciona corretamente antes de migrar o banco de dados para o módulo :shared.

5. Mover o código do banco de dados para o módulo :shared

Nesta etapa, vamos transferir a configuração do banco de dados do Room do app Android para o módulo :shared, permitindo que o banco de dados seja acessível pelo Android e pelo iOS.

Atualizar a configuração build.gradle.kts do módulo :shared

Comece atualizando o build.gradle.kts do módulo :shared para usar dependências multiplataforma do Room.

  1. Adicione os plug-ins KSP e Room:
plugins {
   ...
   // TODO add KSP + ROOM plugins
   alias(libs.plugins.ksp)
   alias(libs.plugins.room)
}
  1. Adicione as dependências room-runtime e sqlite-bundled ao bloco commonMain:
sourceSets {
    commonMain {
        // TODO Add KMP dependencies here
        implementation(libs.androidx.room.runtime)
        implementation(libs.androidx.sqlite.bundled)
    }
}
  1. Adicione uma nova configuração de KSP para cada plataforma de destino adicionando um novo bloco dependencies de nível superior. Para facilitar, adicione-o na parte de baixo do arquivo:
// 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. Também no nível superior, adicione um novo bloco para definir o local do esquema Room:
// Should be its own top level block. For convenience, add at the bottom of the file
room {
   schemaDirectory("$projectDir/schemas")
}
  1. O Gradle sincroniza o projeto

Mover o esquema do Room para o módulo :shared

Mova o diretório androidApp/schemas para a pasta raiz do módulo :shared ao lado da pasta src/:

De: e1ee37a3f3a10b35.png

Para: ba3c9eb617828bac.png

Mover DAOs e entidades

Agora que você adicionou as dependências do Gradle necessárias ao módulo compartilhado do KMP, é necessário mover os DAOs e entidades do módulo :androidApp para o módulo :shared.

Isso envolve mover arquivos para os respectivos locais no conjunto de origem commonMain no módulo :shared.

Mover o modelo Fruittie

Você pode aproveitar a funcionalidade Refactor → Move para mudar entre módulos sem interromper as importações:

  1. Localize o arquivo androidApp/src/main/kotlin/.../model/Fruittie.kt, clique com o botão direito do mouse e selecione Refactor → Move (ou pressione F6):c893e12b8bf683ae.png
  2. Na caixa de diálogo Move, selecione o ícone ... ao lado do campo Destination directory. 1d51c3a410e8f2c3.png
  3. Selecione o conjunto de origem commonMain na caixa de diálogo Choose Destination Directory e clique em OK. Talvez seja necessário desativar a caixa de seleção Show only existing source roots. f61561feb28a6445.png
  4. Clique no botão Refactor para mover o arquivo.

Mover modelos CartItem e CartItemWithFruittie

Para o arquivo androidApp/.../model/CartItem.kt, siga estas etapas:

  1. Abra o arquivo, clique com o botão direito do mouse na classe CartItem e selecione Refactor > Move.
  2. Isso abre a mesma caixa de diálogo Move, mas nesse caso, você também marca a caixa do membro CartItemWithFruittie.
  3. a25022cce5cee5e0.png Continue selecionando o ícone ... e o conjunto de origem commonMain, assim como você fez com o arquivo Fruittie.kt.

Mover DAOs e AppDatabase

Siga as mesmas etapas para os seguintes arquivos (você pode selecionar os três ao mesmo tempo):

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

Atualizar o AppDatabase compartilhado para funcionar em várias plataformas

Agora que você moveu as classes do banco de dados para o módulo :shared, é necessário ajustá-las para gerar as implementações necessárias nas duas plataformas.

  1. Abra o arquivo /shared/src/commonMain/kotlin/com/example/fruitties/kmptutorial/android/database/AppDatabase.kt.
  2. Adicione a seguinte implementação 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. Adicione a anotação @ConstructedBy(AppDatabaseConstructor::class) à classe AppDatabase:
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

Mover a criação do banco de dados para o módulo :shared

Em seguida, você move a configuração do Room específica do Android do módulo :androidApp para o módulo :shared. Isso é necessário porque na próxima etapa você vai remover a dependência Room do módulo :androidApp.

  1. Localize o arquivo androidApp/.../di/DatabaseModule.kt.
  2. Selecione o conteúdo da função providesAppDatabase, clique com o botão direito do mouse e selecione Refactor > Extract Function to Scope: da4d97319f9a0e8c.png
  3. Selecione DatabaseModule.kt no menu. 5e540a1eec6e3493.png Isso move o conteúdo para uma função appDatabase global. Pressione Enter para confirmar o nome da função. e2fb113d66704a36.png
  4. Remova o modificador de visibilidade private para tornar a função pública.
  5. Mova a função para o módulo :shared clicando com o botão direito do mouse em Refactor > Move.
  6. Na caixa de diálogo Move, selecione o ícone ... ao lado do campo Destination directory. e2101005f2ef4747.png
  7. Na caixa de diálogo Choose Destination Directory, selecione o conjunto de origem shared >androidMain e a pasta /shared/src/androidMain/. Em seguida, clique em OK.73d244941c68dc85.png
  8. Mude o sufixo no campo To package de .di para .databaseac5cf30d32871e2c.png
  9. Clique em Refactor.

Limpeza do código desnecessário do :androidApp

Nesse ponto, você moveu o banco de dados do Room para o módulo multiplataforma, e nenhuma das dependências do Room é necessária no módulo :androidApp. Portanto, é possível removê-las.

  1. Abra o arquivo build.gradle.kts no módulo :androidApp.
  2. Remova as dependências e a configuração, conforme mostrado no snippet abaixo:
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. O Gradle sincroniza o projeto.

Criar e executar o app Android

Execute o app Android Fruitties e confira se ele funciona corretamente e usa o banco de dados do módulo :shared. Se você tiver adicionado itens ao carrinho antes, eles também vão aparecer neste ponto, mesmo que o banco de dados do Room esteja localizado no módulo :shared.

6. Preparar o Room para uso no iOS

Para preparar ainda mais o banco de dados do Room para a plataforma iOS, você precisa configurar um código de suporte no módulo :shared para uso na próxima etapa.

Ativar a criação de banco de dados para o app iOS

A primeira coisa a fazer é adicionar um criador de banco de dados específico para iOS.

  1. Adicione um novo arquivo ao módulo :shared no conjunto de origem iosMain com o nome AppDatabase.ios.kt: dcb46ba560298865.png
  2. Adicione as seguintes funções auxiliares. Essas funções serão usadas pelo app iOS para receber uma instância do banco de dados do 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")
        }
    }
}

Adicionar o sufixo "Entity" às entidades do Room

Como você está adicionando wrappers para as entidades do Room no Swift, é melhor que os nomes das entidades do Room sejam diferentes dos nomes dos wrappers. Para garantir que esse seja o caso, adicione o sufixo Entity às entidades do Room usando a anotação @ObjCName.

Abra o arquivo Fruittie.kt no módulo :shared e adicione a anotação @ObjCName à entidade Fruittie. Como essa anotação é experimental, talvez seja necessário adicionar a anotação @OptIn(ExperimentalObjC::class) ao arquivo.

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

Em seguida, faça o mesmo para a entidade CartItem no arquivo 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. Usar o Room no app iOS

O app iOS é um app já pronto que usa Core Data. Neste codelab, você não precisa se preocupar em migrar dados do banco de dados, porque este app é apenas um protótipo. Se você estiver migrando um app de produção para o KMP, vai precisar escrever funções para ler o banco de dados atual do Core Data e inserir esses itens no banco de dados do Room na primeira inicialização após a migração.

Abrir o projeto do Xcode

Abra o projeto iOS no Xcode navegando até a pasta /iosApp/ e abrindo Fruitties.xcodeproj no app associado.

54836291a243ebe9.png

Remover classes de entidade do Core Data

Primeiro, você precisa remover as classes de entidade do Core Data para abrir espaço para os wrappers de entidade que serão criados mais tarde. Dependendo do tipo de dados armazenados pelo app no Core Data, é possível remover as entidades do Core Data por completo ou mantê-las para fins de migração de dados. Para este tutorial, basta removê-los, já que não é necessário migrar os dados atuais.

No Xcode:

  1. Acesse o Project Navigator.
  2. Acesse a pasta "Resources".
  3. Abra o arquivo Fruitties.
  4. Clique e exclua cada entidade.

7ad742d991d76b1c.png

Para disponibilizar essas mudanças no código, limpe e recrie o projeto.

Isso vai fazer com que o build falhe com os seguintes erros, o que é esperado.

e3e107bf0387eeab.png

Como criar wrappers de entidade

Em seguida, vamos criar wrappers de entidade para as entidades Fruittie e CartItem para processar facilmente as diferenças de API entre as entidades do Room e do Core Data.

Esses wrappers vão nos ajudar a fazer a transição do Core Data para o Room, minimizando a quantidade de código que precisa ser atualizado imediatamente. No futuro, tente substituir esses wrappers pelo acesso direto às entidades do Room.

Por enquanto, vamos criar um wrapper para a classe FruittieEntity, atribuindo propriedades opcionais a ela.

Criar um wrapper de FruittieEntity

  1. Crie um novo arquivo Swift no diretório Sources/Repository clicando com o botão direito do mouse no nome do diretório e selecionando New File from Template… cce140b2fb3c2da8.png 6a0d4fa4292ddd4f.png
  2. Nomeie-o como Fruittie e confira se apenas o destino "Fruitties" está selecionado, e não o destino de teste. 827b9019b0a32352.png
  3. Adicione o código abaixo ao novo arquivo Fruittie:
import sharedKit

struct Fruittie: Hashable {
   let entity: FruittieEntity

   var id: Int64 {
       entity.id
   }

   var name: String? {
       entity.name
   }

   var fullName: String? {
       entity.fullName
   }
}

A estrutura Fruittie envolve a classe FruittieEntity, tornando as propriedades opcionais e passando pelas propriedades da entidade. Além disso, fazemos com que o struct Fruittie obedeça ao protocolo Hashable para que ele possa ser usado na visualização ForEach da SwiftUI.

Criar um wrapper de CartItemEntity

Em seguida, crie um wrapper semelhante para a classe CartItemEntity.

Crie um novo arquivo Swift chamado CartItem.swift no diretório 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)
   }
}

Como a classe CartItem original do Core Data tinha uma propriedade Fruittie, também incluímos uma propriedade Fruittie na estrutura CartItem. Embora a classe CartItem não tenha propriedades opcionais, a propriedade count tem um tipo diferente na entidade do Room.

Atualizar repositórios

Agora que os wrappers de entidade estão em vigor, é necessário atualizar o DefaultCartRepository e o DefaultFruittieRepository para usar o Room em vez do Core Data.

Atualizar DefaultCartRepository

Vamos começar com a classe DefaultCartRepository, que é mais simples.

Abra o arquivo CartRepository.swift no diretório Sources/Repository.

  1. Primeiro, substitua a importação CoreData por sharedKit:
import sharedKit
  1. Em seguida, remova a propriedade NSManagedObjectContext e substitua-a por uma propriedade CartDao:
// Remove
private let managedObjectContext: NSManagedObjectContext

// Replace with
private let cartDao: any CartDao
  1. Atualize o construtor init para inicializar a nova propriedade cartDao:
init(cartDao: any CartDao) {
    self.cartDao = cartDao
}
  1. Em seguida, atualize o método addToCart. Esse método precisava extrair os itens do carrinho do Core Data, mas nossa implementação do Room não exige isso. Em vez disso, ele insere um novo item ou incrementa a contagem de um item do carrinho.
func addToCart(fruittie: Fruittie) async throws {
    try await cartDao.insertOrIncreaseCount(fruittie: fruittie.entity)
}
  1. Por fim, atualize o método getCartItems(). Esse método vai chamar o método getAll() no CartDao e mapear as entidades CartItemWithFruittie para nossos wrappers CartItem.
func getCartItems() -> AsyncStream<[CartItem]> {
    return cartDao.getAll().map { entities in
        entities.map(CartItem.init(entity:))
    }.eraseToStream()
}

Atualizar DefaultFruittieRepository

Para migrar a classe DefaultFruittieRepository, aplicamos mudanças semelhantes às que fizemos para o DefaultCartRepository.

Atualize o arquivo FruittieRepository para o seguinte:

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

Substituir wrappers de propriedade @FetchRequest

Também precisamos substituir os wrappers de propriedade @FetchRequest nas visualizações da SwiftUI. O wrapper de propriedade @FetchRequest é usado para buscar dados do Core Data e observar mudanças. Portanto, não é possível usá-lo com entidades do Room. Em vez disso, vamos usar o UIModel para acessar os dados dos repositórios.

  1. Abra o CartView no arquivo Sources/UI/CartView.swift.
  2. Substitua a implementação pelo seguinte:
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)")
                    }
                }
            }
        }
    }
}

Atualizar a ContentView

Atualize a ContentView no arquivo Sources/View/ContentView.swift para transmitir o FruittieUIModel à CartView.

CartView(uiModel: uiModel)

Como atualizar o DataController

A classe DataController no aplicativo iOS é responsável por configurar a pilha do Core Data. Como estamos nos afastando do Core Data, precisamos atualizar o DataController para inicializar o banco de dados do Room.

  1. Abra o arquivo DataController.swift em Sources/Database.
  2. Adicione a importação sharedKit.
  3. Remova a importação CoreData.
  4. Crie uma instância do banco de dados do Room na classe DataController.
  5. Por fim, remova a chamada de método loadPersistentStores do inicializador DataController.

A classe atualizada ficará assim:

import Combine
import sharedKit

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

Como atualizar a injeção de dependência

A classe AppContainer no aplicativo iOS é responsável por inicializar o gráfico de dependências. Como atualizamos os repositórios para usar o Room em vez do Core Data, precisamos atualizar o AppContainer para transmitir os DAOs do Room aos repositórios.

  1. Abra o AppContainer.swift na pasta Sources/DI.
  2. Adicione a importação sharedKit.
  3. Remova a propriedade managedObjectContext da classe AppContainer.
  4. Mude as inicializações de DefaultFruittieRepository e DefaultCartRepository transmitindo DAOs do Room da instância AppDatabase fornecida pelo DataController.

Quando terminar, a classe vai ficar assim:

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 fim, atualize FruittiesApp em main.swift para remover o managedObjectContext:

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

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

Criar e executar o app iOS

Por fim, depois de criar e executar o app pressionando ⌘R, ele vai começar com o banco de dados migrado do Core Data para o Room.

5d2ae9438747f8f6.png

8. Parabéns

Parabéns! Você migrou apps Android e iOS independentes para uma camada de dados compartilhada usando o KMP do Room.

Para referência, confira a comparação das arquiteturas de apps para ver o que fizemos:

Antes

Android

iOS

Diagrama de arquitetura do app Android antes da integração do KMP

Diagrama de arquitetura do app iOS antes da integração do KMP

Após a migração para Android

bcd8c29b00f67c19.png

Saiba mais (links em inglês)