La libreria di persistenza Room fornisce un livello di astrazione su SQLite per consentire un accesso più efficace al database, sfruttando al contempo tutta la potenza di SQLite. Questa pagina si concentra sull'utilizzo di Room nei progetti Kotlin Multiplatform (KMP). Per saperne di più sull'utilizzo di Room, consulta Salva i dati in un database locale utilizzando Room o i nostri esempi ufficiali.
Configura le dipendenze
Per configurare Room nel tuo progetto KMP, aggiungi le dipendenze per gli artefatti nel file build.gradle.kts del tuo modulo KMP.
Definisci le dipendenze nel file libs.versions.toml:
[versions]
room3 = "3.1.0-alpha03"
sqlite = "2.7.1"
ksp = "<kotlinCompatibleKspVersion>"
[libraries]
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version.ref = "room3" }
androidx-room3-compiler = { module = "androidx.room3:room3-compiler", version.ref = "room3" }
# Optional SQLite Wrapper
androidx-room3-sqlite-wrapper = { module = "androidx.room3:room3-sqlite-wrapper", version.ref = "room3" }
[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
androidx-room3 = { id = "androidx.room3", version.ref = "room3" }
Aggiungi il plug-in Room Gradle per configurare gli schemi Room e il plug-in KSP.
plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.androidx.room3)
}
Aggiungi la dipendenza di runtime di Room e la libreria SQLite in bundle:
commonMain.dependencies {
implementation(libs.androidx.room3.runtime)
implementation(libs.androidx.sqlite.bundled)
}
// Optional when using Room SQLite Wrapper
androidMain.dependencies {
implementation(libs.androidx.room3.sqlite.wrapper)
}
Aggiungi le dipendenze KSP al blocco dependencies root. Devi aggiungere
tutti i target utilizzati dalla tua app. Per saperne di più, consulta KSP con Kotlin Multiplatform.
dependencies {
add("kspAndroid", libs.androidx.room3.compiler)
add("kspIosSimulatorArm64", libs.androidx.room3.compiler)
add("kspIosX64", libs.androidx.room3.compiler)
add("kspIosArm64", libs.androidx.room3.compiler)
// Add any other platform target you use in your project, for example kspDesktop
}
Definisci la directory dello schema Room. Per ulteriori informazioni, consulta Imposta la posizione dello schema utilizzando il plug-in Room Gradle.
room {
schemaDirectory("$projectDir/schemas")
}
Definisci le classi di database
Devi creare una classe di database annotata con @Database insieme ai DAO e alle entità all'interno del set di risorse comune del modulo KMP condiviso. Se inserisci queste classi in origini comuni, potrai condividerle su tutte le piattaforme di destinazione.
// shared/src/commonMain/kotlin/Database.kt @Database(entities = [TodoEntity::class], version = 1) @ConstructedBy(AppDatabaseConstructor::class) abstract class AppDatabase : RoomDatabase() { abstract fun getDao(): TodoDao } // The Room compiler generates the `actual` implementations. @Suppress("KotlinNoActualForExpect") expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> { override fun initialize(): AppDatabase }
Quando dichiari un oggetto expect con l'interfaccia RoomDatabaseConstructor, il compilatore Room genera le implementazioni actual. Android Studio potrebbe mostrare il seguente avviso, che puoi
eliminare con @Suppress("KotlinNoActualForExpect"):
Expected object 'AppDatabaseConstructor' has no actual declaration in module`
Successivamente, definisci una nuova interfaccia DAO o spostane una esistente in commonMain:
// shared/src/commonMain/kotlin/TodoDao.kt @Dao interface TodoDao { @Insert suspend fun insert(item: TodoEntity) @Query("SELECT count(*) FROM TodoEntity") suspend fun count(): Int @Query("SELECT * FROM TodoEntity") fun getAllAsFlow(): Flow<List<TodoEntity>> }
Definisci o sposta le tue entità in commonMain:
// shared/src/commonMain/kotlin/TodoEntity.kt @Entity data class TodoEntity( @PrimaryKey(autoGenerate = true) val id: Long = 0, val title: String, val content: String )
Crea builder di database specifici per la piattaforma
Devi definire un generatore di database per creare un'istanza di Room su ogni piattaforma. Questa è l'unica parte dell'API che devi definire nei set di risorse specifici della piattaforma, a causa delle differenze nelle API del file system.
Android
Su Android, in genere ottieni la posizione del database utilizzando l'API Context.getDatabasePath. Per creare l'istanza di database, specifica un Context e il percorso del database.
// shared/src/androidMain/kotlin/Database.android.kt fun getDatabaseBuilder(context: Context): RoomDatabase.Builder<AppDatabase> { val appContext = context.applicationContext val dbFile = appContext.getDatabasePath("my_room.db") return Room.databaseBuilder<AppDatabase>( context = appContext, name = dbFile.absolutePath ) }
iOS
Per creare l'istanza del database su iOS, fornisci un percorso del database utilizzando NSFileManager, che di solito si trova in NSDocumentDirectory.
// shared/src/iosMain/kotlin/Database.ios.kt fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> { val dbFilePath = documentDirectory() + "/my_room.db" return Room.databaseBuilder<AppDatabase>( name = dbFilePath, ) } @OptIn(ExperimentalForeignApi::class) private fun documentDirectory(): String { val documentDirectory = NSFileManager.defaultManager.URLForDirectory( directory = NSDocumentDirectory, inDomain = NSUserDomainMask, appropriateForURL = null, create = false, error = null, ) return requireNotNull(documentDirectory?.path) }
Desktop JVM
Per creare l'istanza del database, fornisci un percorso del database utilizzando le API Java o Kotlin.
// shared/src/jvmMain/kotlin/Database.desktop.kt
fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
val dbFile = File(System.getProperty("java.io.tmpdir"), "my_room.db")
return Room.databaseBuilder<AppDatabase>(
name = dbFile.absolutePath,
)
}
Crea un'istanza del database
Una volta ottenuto RoomDatabase.Builder da uno dei costruttori specifici della piattaforma, puoi configurare il resto del database Room nel codice comune insieme alla creazione dell'istanza del database effettiva.
// shared/src/commonMain/kotlin/Database.kt fun getRoomDatabase( builder: RoomDatabase.Builder<AppDatabase> ): AppDatabase { return builder .setDriver(BundledSQLiteDriver()) .setQueryCoroutineContext(Dispatchers.IO) .build() }
Seleziona un driver SQLite
Lo snippet di codice precedente chiama la funzione di creazione setDriver per definire il driver SQLite che il database Room deve utilizzare. Questi driver variano in base alla piattaforma di destinazione. Gli snippet di codice precedenti utilizzano BundledSQLiteDriver.
È il driver consigliato che include SQLite compilato dall'origine, che fornisce la versione più coerente e aggiornata di SQLite su tutte le piattaforme.
Se vuoi utilizzare SQLite fornito dal sistema operativo, utilizza l'API setDriver nei set di risorse specifici della piattaforma che specificano un driver specifico della piattaforma. Consulta
Implementazioni dei driver per le descrizioni delle implementazioni dei driver
disponibili. Puoi utilizzare una delle seguenti opzioni:
AndroidSQLiteDriverinandroidMainNativeSQLiteDriveriniosMain
Per utilizzare NativeSQLiteDriver, devi fornire un'opzione del linker -lsqlite3 in modo che l'app per iOS si colleghi dinamicamente a SQLite di sistema.
// shared/build.gradle.kts
kotlin {
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "TodoApp"
isStatic = true
// Required when using NativeSQLiteDriver
linkerOpts.add("-lsqlite3")
}
}
}
Imposta un contesto di coroutine
Un oggetto RoomDatabase su Android può essere configurato facoltativamente con esecutori di applicazioni condivisi utilizzando RoomDatabase.Builder.setQueryExecutor per eseguire operazioni sul database.
Poiché gli esecutori non sono compatibili con KMP, l'API setQueryExecutor di Room non è disponibile in commonMain. L'oggetto RoomDatabase deve invece essere configurato con un CoroutineContext, che puoi impostare utilizzando RoomDatabase.Builder.setCoroutineContext. Se non imposti un contesto, l'oggetto RoomDatabase viene impostato su Dispatchers.IO per impostazione predefinita.
Minificazione e offuscamento
Se il progetto è minimizzato o offuscato, devi includere la seguente regola ProGuard in modo che Room possa trovare l'implementazione generata della definizione del database:
-keep class * extends androidx.room3.RoomDatabase { <init>(); }
Esegui la migrazione a Kotlin Multiplatform
Room è stata originariamente sviluppata come libreria Android e successivamente è stata migrata a KMP con un'attenzione particolare alla compatibilità con le API. La versione KMP di Room è leggermente diversa tra le piattaforme e rispetto alla versione specifica per Android. Queste differenze sono elencate e descritte di seguito.
Esegui la migrazione da Support SQLite a SQLite Driver
Qualsiasi utilizzo di SupportSQLiteDatabase e di altre API in androidx.sqlite.db deve essere rifattorizzato con le API SQLite Driver, perché le API in androidx.sqlite.db sono solo per Android (nota il pacchetto diverso rispetto al pacchetto KMP).
Per la compatibilità con le versioni precedenti e finché RoomDatabase è configurato con un SupportSQLiteOpenHelper.Factory (ad esempio, non è impostato SQLiteDriver), Room si comporta in "modalità di compatibilità" in cui le API Support SQLite e SQLite Driver funzionano come previsto. In questo modo, puoi eseguire migrazioni incrementali, senza dover convertire tutti gli utilizzi di Support SQLite in SQLite Driver in un'unica modifica.
Utilizza il wrapper SQLite Room
L'artefatto androidx.room3:room3-sqlite-wrapper fornisce API per collegare SQLiteDriver e SupportSQLiteDatabase durante la migrazione.
Per ottenere un SupportSQLiteDatabase da un RoomDatabase configurato con un
SQLiteDriver, utilizza la nuova funzione di estensione
RoomDatabase.getSupportWrapper. Questo wrapper di compatibilità aiuta a mantenere gli utilizzi esistenti di SupportSQLiteDatabase, che spesso ottieni da RoomDatabase.openHelper.writableDatabase, adottando al contempo SQLiteDriver, in particolare per i codebase con utilizzi estesi dell'API SupportSQLite che vogliono utilizzare BundledSQLiteDriver.
Converti le sottoclassi di migrazione
Le sottoclassi di migrazione devono essere migrate alle controparti del driver SQLite:
Multipiattaforma Kotlin
Sottoclassi di migrazione
object Migration_1_2 : Migration(1, 2) { override suspend fun migrate(connection: SQLiteConnection) { // ... } }
Sottoclassi di specifiche di migrazione automatica
class AutoMigrationSpec_1_2 : AutoMigrationSpec { override suspend fun onPostMigrate(connection: SQLiteConnection) { // ... } }
Solo Android
Sottoclassi di migrazione
object Migration_1_2 : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
// …
}
}
Sottoclassi di specifiche di migrazione automatica
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
// …
}
}
Callback di conversione del database
I callback del database devono essere migrati alle controparti del driver SQLite:
Multipiattaforma Kotlin
object MyRoomCallback : RoomDatabase.Callback() { override suspend fun onCreate(connection: SQLiteConnection) { // ... } override suspend fun onDestructiveMigration(connection: SQLiteConnection) { // ... } override suspend fun onOpen(connection: SQLiteConnection) { // ... } }
Solo Android
object MyRoomCallback : RoomDatabase.Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
// …
}
override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
// …
}
override fun onOpen(db: SupportSQLiteDatabase) {
// …
}
}
Converti le funzioni DAO di @RawQuery
Le funzioni annotate con @RawQuery compilate per piattaforme non Android dovranno dichiarare un parametro di tipo RoomRawQuery anziché SupportSQLiteQuery.
Multipiattaforma Kotlin
Definisci la query non elaborata
@Dao interface TodoDao { @RawQuery suspend fun getTodos(query: RoomRawQuery): List<TodoEntity> }
Un RoomRawQuery può quindi essere utilizzato per creare una query in fase di runtime:
suspend fun AppDatabase.getTodosWithLowercaseTitle(title: String): List<TodoEntity> { val query = RoomRawQuery( sql = "SELECT * FROM TodoEntity WHERE title = ?", onBindStatement = { it.bindText(1, title.lowercase()) } ) return getDao().getTodos(query) }
Solo Android
Definisci la query non elaborata
@Dao
interface TodoDao {
@RawQuery
suspend fun getTodos(query: SupportSQLiteQuery): List<TodoEntity>
}
Un SimpleSQLiteQuery può quindi essere utilizzato per creare una query in fase di runtime:
suspend fun AndroidOnlyDao.getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
val query = SimpleSQLiteQuery(
query = "SELECT * FROM TodoEntity WHERE title = ?",
bindArgs = arrayOf(title.lowercase())
)
return getTodos(query)
}
Converti le funzioni DAO di blocco
Room sfrutta la libreria asincrona kotlinx.coroutines ricca di funzionalità
che Kotlin offre per più piattaforme. Per una funzionalità ottimale, le funzioni suspend vengono applicate per i DAO compilati in un progetto KMP, ad eccezione dei DAO implementati in androidMain per mantenere la compatibilità con le versioni precedenti del codebase esistente. Quando utilizzi Room per KMP, tutte le funzioni DAO compilate per piattaforme non Android devono essere funzioni suspend.
Multipiattaforma Kotlin
Sospensione delle query
@Query("SELECT * FROM Todo") suspend fun getAllTodos(): List<Todo>
Sospensione delle transazioni
@Transaction suspend fun transaction() { // ... }
Solo Android
Query di blocco
@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>
Transazioni di blocco
@Transaction
fun blockingTransaction() { … }
Converti i tipi reattivi in Flow
Non tutte le funzioni DAO devono essere funzioni di sospensione. Le funzioni DAO che restituiscono tipi reattivi come LiveData o Flowable di RxJava non devono essere convertite in funzioni di sospensione. Tuttavia, alcuni tipi, come LiveData, non sono compatibili con KMP. Le funzioni DAO con tipi restituiti reattivi devono essere migrate ai flussi di coroutine.
Multipiattaforma Kotlin
Tipi reattivi Flows
@Query("SELECT * FROM Todo") fun getTodosFlow(): Flow<List<Todo>>
Solo Android
Tipi reattivi come LiveData o Flowable di RxJava
@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>
Converti le API Transaction
Le API delle transazioni del database per Room KMP possono distinguere tra transazioni di scrittura (useWriterConnection) e di lettura (useReaderConnection).
Multipiattaforma Kotlin
val database: RoomDatabase = getRoomDatabase(builder) database.useWriterConnection { transactor -> transactor.immediateTransaction { // perform database operations in transaction. } }
Solo Android
val database: RoomDatabase = …
database.withTransaction {
// perform database operations in transaction
}
Transazioni di scrittura
Utilizza le transazioni di scrittura per assicurarti che più query scrivano i dati in modo atomico, in modo che i lettori possano accedere ai dati in modo coerente. Puoi farlo utilizzando
useWriterConnection con uno dei tre tipi di transazione:
immediateTransaction: in modalità Write-Ahead Logging (WAL) (predefinita), questo tipo di transazione acquisisce un blocco all'avvio, ma i lettori possono continuare a leggere. Questa è la scelta preferita per la maggior parte dei casi.deferredTransaction: la transazione non acquisirà un blocco fino alla prima istruzione di scrittura. Utilizza questo tipo di transazione come ottimizzazione quando non sai se sarà necessaria un'operazione di scrittura all'interno della transazione. Ad esempio, se avvii una transazione per eliminare i brani da una playlist fornendo solo il nome della playlist e questa non esiste, non è necessaria alcuna operazione di scrittura (eliminazione).exclusiveTransaction: questa modalità si comporta in modo identico aimmediateTransactionnella modalità WAL. In altre modalità di journaling, impedisce ad altre connessioni al database di leggere il database mentre la transazione è in corso.
Transazioni di lettura
Utilizza le transazioni di lettura per leggere in modo coerente dal database più volte. Ad esempio, quando hai due o più query separate e non utilizzi una clausola JOIN. Nelle connessioni dei lettori sono consentite solo le transazioni differite. Il tentativo di avviare una transazione immediata o esclusiva in una connessione di lettura genererà un'eccezione, poiché queste sono considerate operazioni di "scrittura".
val database: RoomDatabase = getRoomDatabase(builder) database.useReaderConnection { transactor -> transactor.deferredTransaction { // perform database operations in transaction. } }
Non disponibile in Kotlin Multiplatform
Alcune delle API disponibili per Android non sono disponibili in Kotlin Multiplatform.
Query callback
Le seguenti API per la configurazione dei callback delle query non sono disponibili in common e, di conseguenza, non sono disponibili su piattaforme diverse da Android.
RoomDatabase.Builder.setQueryCallbackRoomDatabase.QueryCallback
Database con chiusura automatica
L'API per attivare la chiusura automatica dopo un timeout,
RoomDatabase.Builder.setAutoCloseTimeout, è disponibile solo su Android.
Database predefinito
Le seguenti API per creare un RoomDatabase utilizzando un database esistente (un database pre-pacchettizzato) non sono disponibili in comune e non sono quindi disponibili in piattaforme diverse da Android:
RoomDatabase.Builder.createFromAssetRoomDatabase.Builder.createFromFileRoomDatabase.Builder.createFromInputStreamRoomDatabase.PrepackagedDatabaseCallback
Annullamento della convalida multi-istanza
L'API per attivare l'annullamento della convalida multi-istanza, RoomDatabase.Builder.enableMultiInstanceInvalidation, è disponibile solo su Android e non è disponibile su piattaforme comuni o di altro tipo.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Codelab: Esegui la migrazione delle app esistenti su Room KMP
- Codelab Inizia a utilizzare KMP
- Salva i dati nel database locale con Room