La libreria di persistenza Room fornisce un livello di astrazione su SQLite per consentire un accesso più affidabile al database sfruttando tutta la potenza di SQLite. Questa pagina si concentra sull'utilizzo di Room nei progetti Kotlin Multiplatform (KMP). Per ulteriori informazioni sull'utilizzo di Room, consulta Salvare i dati in un database locale utilizzando Room o i nostri esempi ufficiali.
Configurare le dipendenze
Per configurare Room nel tuo progetto KMP, aggiungi le dipendenze per gli artefatti nel file build.gradle.kts
del modulo KMP.
Definisci le dipendenze nel file libs.versions.toml
:
[versions]
room = "2.7.2"
sqlite = "2.5.2"
ksp = "<kotlinCompatibleKspVersion>"
[libraries]
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
androidx-room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
androidx-room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
# Optional SQLite Wrapper available in version 2.8.0 and higher
androidx-room-sqlite-wrapper = { module = "androidx.room:room-sqlite-wrapper", version.ref = "room" }
[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
androidx-room = { id = "androidx.room", version.ref = "room" }
Aggiungi il plug-in Gradle di Room per configurare gli schemi di Room e il plug-in KSP
plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.androidx.room)
}
Aggiungi la dipendenza del runtime di Room e la libreria SQLite in bundle:
commonMain.dependencies {
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.sqlite.bundled)
}
// Optional when using Room SQLite Wrapper
androidMain.dependencies {
implementation(libs.androidx.room.sqlite.wrapper)
}
Aggiungi le dipendenze KSP al blocco root dependencies
. Tieni presente che devi aggiungere tutti i target utilizzati dalla tua app. Per saperne di più, consulta KSP con
Kotlin Multiplatform.
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)
// Add any other platform target you use in your project, for example kspDesktop
}
Definisci la directory dello schema della stanza. Per ulteriori informazioni, vedi Impostare la posizione dello schema utilizzando il plug-in Gradle per Room.
room {
schemaDirectory("$projectDir/schemas")
}
Definisci le classi di database
Devi creare una classe di database annotata con @Database
insieme a DAO
ed entità all'interno del set di origini comuni 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 di actual
. Android Studio potrebbe mostrare il seguente avviso, che puoi
eliminare con @Suppress("KotlinNoActualForExpect")
:
Expected object 'AppDatabaseConstructor' has no actual declaration in module`
A questo punto, definisci una nuova interfaccia DAO o sposta 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 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 il generatore di database specifico della piattaforma
Devi definire un generatore di database per creare un'istanza di Room su ogni piattaforma. Questa è l'unica parte dell'API che deve essere inclusa in set di origini specifici della piattaforma a causa delle differenze nelle API del file system.
Android
Su Android, la posizione del database viene in genere ottenuta tramite l'API Context.getDatabasePath()
. Per creare l'istanza di database, specifica un
Context
insieme al 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
, in genere situato 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,
)
}
private fun documentDirectory(): String {
val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
directory = NSDocumentDirectory,
inDomain = NSUserDomainMask,
appropriateForURL = null,
create = false,
error = null,
)
return requireNotNull(documentDirectory?.path)
}
JVM (computer)
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,
)
}
Istanziare il database
Una volta ottenuto RoomDatabase.Builder
da uno dei costruttori specifici della piattaforma, puoi configurare il resto del database Room nel codice comune insieme all'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 quale
driver SQLite deve utilizzare il database Room. Questi driver variano in base alla
piattaforma di destinazione. Gli snippet di codice precedenti utilizzano BundledSQLiteDriver
.
Questo è il driver consigliato che include SQLite compilato dall'origine, il 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 origini specifici della piattaforma che specificano un driver specifico della piattaforma. Per le descrizioni delle implementazioni dei driver disponibili, consulta la sezione
Implementazioni dei driver. Puoi utilizzare uno dei seguenti elementi:
AndroidSQLiteDriver
inandroidMain
NativeSQLiteDriver
iniosMain
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")
}
}
}
(Facoltativo) Impostare un contesto di coroutine
Un oggetto RoomDatabase
su Android può essere configurato facoltativamente con executor di applicazioni condivisi utilizzando RoomDatabase.Builder.setQueryExecutor()
per eseguire operazioni sul database.
Poiché gli executor non sono compatibili con KMP, l'API setQueryExecutor()
di Room non è
disponibile in commonMain
. L'oggetto RoomDatabase
deve essere configurato
con un CoroutineContext
, che può essere impostato utilizzando
RoomDatabase.Builder.setCoroutineContext()
. Se non viene impostato alcun contesto, l'oggetto
RoomDatabase
utilizzerà Dispatchers.IO
per impostazione predefinita.
Minimizzazione e offuscamento
Se il progetto è ridotto 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.room.RoomDatabase { <init>(); }
Esegui la migrazione a Kotlin Multiplatform
Room è stata originariamente sviluppata come libreria Android e successivamente è stata migrata a KMP con particolare attenzione alla compatibilità delle API. La versione KMP di Room differisce leggermente tra le piattaforme e dalla 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 sottoposto a refactoring con le API del driver SQLite,
perché le API in androidx.sqlite.db
sono solo per Android (nota il
pacchetto diverso da quello KMP).
Per la compatibilità con le versioni precedenti e finché RoomDatabase
è configurato
con un SupportSQLiteOpenHelper.Factory
(ad esempio, non è impostato alcun SQLiteDriver
),
Room si comporta in "modalità compatibilità", in cui le API Support SQLite e
SQLite Driver funzionano come previsto. Ciò consente migrazioni incrementali, in modo da non dover convertire tutti gli utilizzi di Support SQLite in SQLite Driver in un'unica modifica.
Converti sottoclassi di migrazioni
Le sottoclassi di migrazioni devono essere migrate alle controparti del driver SQLite:
Kotlin Multiplatform
Sottoclassi di migrazione
object Migration_1_2 : Migration(1, 2) {
override fun migrate(connection: SQLiteConnection) {
// …
}
}
Sottoclassi della specifica di migrazione automatica
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(connection: SQLiteConnection) {
// …
}
}
Solo Android
Sottoclassi di migrazione
object Migration_1_2 : Migration(1, 2) {
override fun migrate(db: SupportSQLiteDatabase) {
// …
}
}
Sottoclassi della specifica di migrazione automatica
class AutoMigrationSpec_1_2 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
// …
}
}
Converti callback database
I callback del database devono essere migrati alle controparti del driver SQLite:
Kotlin Multiplatform
object MyRoomCallback : RoomDatabase.Callback() {
override fun onCreate(connection: SQLiteConnection) {
// …
}
override fun onDestructiveMigration(connection: SQLiteConnection) {
// …
}
override 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) {
// …
}
}
Convertire le funzioni DAO @RawQuery
Le funzioni annotate con @RawQuery
compilate per piattaforme non Android
dovranno dichiarare un parametro di tipo RoomRawQuery
anziché
SupportSQLiteQuery
.
Kotlin Multiplatform
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 todoDao().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)
}
Convertire 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 alle DAO compilate in un progetto KMP, ad eccezione delle
DAO implementate in androidMain
per mantenere la compatibilità con le versioni precedenti del
codice esistente. Quando utilizzi Room per KMP, tutte le funzioni DAO compilate per
piattaforme non Android devono essere funzioni suspend
.
Kotlin Multiplatform
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>
Blocco delle transazioni
@Transaction
fun blockingTransaction() { … }
Convertire 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. Alcuni tipi, tuttavia, come LiveData
non sono compatibili con KMP. Le funzioni DAO con tipi restituiti reattivi devono essere migrate ai flussi di coroutine.
Kotlin Multiplatform
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>>
API Convert transaction
Le API di transazione del database per Room KMP possono distinguere tra transazioni di scrittura
(useWriterConnection
) e di lettura (useReaderConnection
).
Kotlin Multiplatform
val database: RoomDatabase = …
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) (impostazione 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 acquisisce 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 brani da una playlist fornendo solo il nome della playlist e la playlist non esiste, non è necessaria alcuna operazione di scrittura (eliminazione).exclusiveTransaction
: questa modalità si comporta in modo identico aimmediateTransaction
nella modalità WAL. In altre modalità di journaling, impedisce ad altre connessioni al database di leggere il database mentre la transazione è in corso.
Leggere le transazioni
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 del lettore genererà un'eccezione, in quanto queste sono considerate operazioni di "scrittura".
val database: RoomDatabase = …
database.useReaderConnection { transactor ->
transactor.deferredTransaction {
// perform database operations in transaction
}
}
Non disponibile in Kotlin Multiplatform
Alcune 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 pertanto non sono disponibili in piattaforme diverse da Android.
RoomDatabase.Builder.setQueryCallback
RoomDatabase.QueryCallback
Intendiamo aggiungere il supporto per il callback delle query in una versione futura di Room.
L'API per configurare un RoomDatabase
con un callback di query
RoomDatabase.Builder.setQueryCallback
insieme all'interfaccia di callback
RoomDatabase.QueryCallback
non sono disponibili in common e quindi non sono disponibili
in altre piattaforme diverse da Android.
Database con chiusura automatica
L'API per attivare la chiusura automatica dopo un timeout, RoomDatabase.Builder.setAutoCloseTimeout
, è disponibile solo su Android e non è disponibile in altre piattaforme.
Database pre-package
Le seguenti API per creare un RoomDatabase
utilizzando un database esistente (ovvero un database preconfigurato) non sono disponibili in common e pertanto non sono disponibili in altre piattaforme diverse da Android. Queste API sono:
RoomDatabase.Builder.createFromAsset
RoomDatabase.Builder.createFromFile
RoomDatabase.Builder.createFromInputStream
RoomDatabase.PrepackagedDatabaseCallback
Intendiamo aggiungere il supporto per i database preconfigurati in una versione futura di Room.
Invalidazione multi-istanza
L'API per abilitare l'invalidazione multi-istanza,
RoomDatabase.Builder.enableMultiInstanceInvalidation
è disponibile solo su
Android e non è disponibile su piattaforme comuni o altre piattaforme.
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Eseguire la migrazione delle app esistenti al codelab Room KMP
- Guida introduttiva al codelab KMP
- Salvare i dati nel database locale con Room