Camera (multipiattaforma Kotlin)

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:

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 a immediateTransaction 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.