Room (multiplateforme Kotlin)

La bibliothèque de persistance Room fournit une couche d'abstraction sur SQLite afin de permettre un accès plus fiable à la base de données, tout en exploitant toute la puissance de SQLite. Cette page explique comment utiliser Room dans les projets Kotlin Multiplateforme (KMP). Pour en savoir plus sur l'utilisation de Room, consultez Enregistrer des données dans une base de données locale à l'aide de Room ou nos exemples officiels.

Configurer les dépendances

La version actuelle de Room compatible avec KMP est la version 2.7.0-alpha01 ou ultérieure.

Pour configurer Room dans votre projet KMP, ajoutez les dépendances des artefacts dans le fichier build.gradle.kts de votre module:

  • androidx.room:room-gradle-plugin : plug-in Gradle permettant de configurer les schémas Room
  • androidx.room:room-compiler : processeur KSP qui génère du code
  • androidx.room:room-runtime : partie d'exécution de la bibliothèque
  • androidx.sqlite:sqlite-bundled : (facultatif) Bibliothèque SQLite groupée

Vous devez également configurer le pilote SQLite de Room. Ces pilotes varient selon la plate-forme cible. Pour en savoir plus sur les implémentations de pilotes disponibles, consultez la section Implémentations de pilotes.

Pour en savoir plus sur la configuration, consultez les ressources suivantes:

Définir les classes de base de données

Vous devez créer une classe de base de données annotée avec @Database, ainsi que des DAO et des entités dans l'ensemble de sources commun de votre module KMP partagé. Placer ces classes dans des sources communes leur permettra d'être partagées sur toutes les plates-formes cibles.

Lorsque vous déclarez un objet expect avec l'interface RoomDatabaseConstructor, le compilateur Room génère les implémentations actual. Android Studio peut générer un avertissement "Expected object 'AppDatabaseConstructor' has no actual declaration in module". Vous pouvez le supprimer avec @Suppress("NO_ACTUAL_FOR_EXPECT").

// 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("NO_ACTUAL_FOR_EXPECT")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
    override fun initialize(): AppDatabase
}

@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>>
}

@Entity
data class TodoEntity(
  @PrimaryKey(autoGenerate = true) val id: Long = 0,
  val title: String,
  val content: String
)

Notez que vous pouvez éventuellement utiliser des déclarations actual / expect pour créer des implémentations Room spécifiques à la plate-forme. Par exemple, vous pouvez ajouter un DAO spécifique à la plate-forme défini dans le code commun à l'aide de expect, puis spécifier les définitions actual avec des requêtes supplémentaires dans des ensembles de sources spécifiques à la plate-forme.

Créer l'outil de création de base de données

Vous devez définir un générateur de base de données pour instancier Room sur chaque plate-forme. Il s'agit de la seule partie de l'API qui doit se trouver dans des ensembles de sources spécifiques à la plate-forme en raison des différences entre les API de système de fichiers. Par exemple, sous Android, l'emplacement de la base de données est généralement obtenu via l'API Context.getDatabasePath(), tandis que sous iOS, l'emplacement de la base de données est obtenu à l'aide de NSFileManager.

Android

Pour créer l'instance de base de données, spécifiez un Contexte avec le chemin d'accès à la base de données.

// shared/src/androidMain/kotlin/Database.kt

fun getDatabaseBuilder(ctx: Context): RoomDatabase.Builder<AppDatabase> {
  val appContext = ctx.applicationContext
  val dbFile = appContext.getDatabasePath("my_room.db")
  return Room.databaseBuilder<AppDatabase>(
    context = appContext,
    name = dbFile.absolutePath
  )
}

iOS

Pour créer l'instance de base de données, fournissez un chemin d'accès à la base de données à l'aide de NSFileManager, qui se trouve généralement dans NSDocumentDirectory.

// shared/src/iosMain/kotlin/Database.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 (ordinateur)

Pour créer l'instance de base de données, fournissez un chemin d'accès à la base de données à l'aide d'API Java ou Kotlin.

// shared/src/jvmMain/kotlin/Database.kt

fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
    val dbFile = File(System.getProperty("java.io.tmpdir"), "my_room.db")
    return Room.databaseBuilder<AppDatabase>(
        name = dbFile.absolutePath,
    )
}

Minimisation et obscurcissement

Si le projet est minimisé ou obscurci, la règle ProGuard suivante doit être incluse pour que Room puisse trouver l'implémentation générée de la définition de la base de données:

-keep class * extends androidx.room.RoomDatabase { <init>(); }

Instanciation de la base de données

Une fois que vous avez obtenu le RoomDatabase.Builder à partir de l'un des constructeurs spécifiques à la plate-forme, vous pouvez configurer le reste de la base de données Room dans le code commun, ainsi que l'instanciation de la base de données.

// shared/src/commonMain/kotlin/Database.kt

fun getRoomDatabase(
    builder: RoomDatabase.Builder<AppDatabase>
): AppDatabase {
  return builder
      .addMigrations(MIGRATIONS)
      .fallbackToDestructiveMigrationOnDowngrade()
      .setDriver(BundledSQLiteDriver())
      .setQueryCoroutineContext(Dispatchers.IO)
      .build()
}

Sélectionner un SQLiteDriver

Les extraits de code précédents utilisent BundledSQLiteDriver. Il s'agit du pilote recommandé qui inclut SQLite compilé à partir de la source, qui fournit la version la plus cohérente et à jour de SQLite sur toutes les plates-formes. Si vous souhaitez utiliser SQLite fourni par l'OS, utilisez l'API setDriver dans des ensembles de sources spécifiques à la plate-forme qui spécifient un pilote spécifique à la plate-forme. Pour Android, vous pouvez utiliser AndroidSQLiteDriver, tandis que pour iOS, vous pouvez utiliser NativeSQLiteDriver. Pour utiliser NativeSQLiteDriver, vous devez fournir une option de l'éditeur de liens afin que l'application iOS s'associe de manière dynamique au SQLite du système.

// 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")
        }
    }
}

Différences

Room a été développé à l'origine en tant que bibliothèque Android, puis migré vers KMP en mettant l'accent sur la compatibilité des API. La version KMP de Room diffère quelque peu d'une plate-forme à l'autre et de la version spécifique à Android. Ces différences sont listées et décrites comme suit.

Fonctions DAO bloquantes

Lorsque vous utilisez Room pour KMP, toutes les fonctions DAO compilées pour des plates-formes autres qu'Android doivent être des fonctions suspend, à l'exception des types de retour réactifs, tels que Flow.

// shared/src/commonMain/kotlin/MultiplatformDao.kt

@Dao
interface MultiplatformDao {
  // ERROR: Blocking function not valid for non-Android targets
  @Query("SELECT * FROM Entity")
  fun blockingQuery(): List<Entity>

  // OK
  @Query("SELECT * FROM Entity")
  suspend fun query(): List<Entity>

  // OK
  @Query("SELECT * FROM Entity")
  fun queryFlow(): Flow<List<Entity>>

  // ERROR: Blocking function not valid for non-Android targets
  @Transaction
  fun blockingTransaction() { // … }

  // OK
  @Transaction
  suspend fun transaction() { // … }
}

Room bénéficie de la bibliothèque kotlinx.coroutines asynchrone aux fonctionnalités nombreuses que Kotlin propose pour plusieurs plates-formes. Pour une fonctionnalité optimale, les fonctions suspend sont appliquées aux DAO compilées dans un projet KMP, à l'exception des DAO spécifiques à Android afin de maintenir la rétrocompatibilité avec le codebase existant.

Différences de fonctionnalités avec KMP

Cette section décrit les différences entre les fonctionnalités des versions KMP et de la plate-forme Android de Room.

Fonctions DAO @RawQuery

Les fonctions annotées avec @RawQuery qui sont compilées pour des plates-formes autres qu'Android devront déclarer un paramètre de type RoomRawQuery au lieu de SupportSQLiteQuery.

@Dao
interface TodoDao {
  @RawQuery
  suspend fun getTodos(query RoomRawQuery): List<TodoEntity>
}

Vous pouvez ensuite utiliser un RoomRawQuery pour créer une requête au moment de l'exécution:

suspend fun getTodosWithLowercaseTitle(title: String): List<TodoEntity> {
  val query = RoomRawQuery(
    sql = "SELECT * FROM TodoEntity WHERE title = ?"
    onBindStatement = {
      it.bindText(1, title.lowercase())
    }
  )
  return todosDao.getTodos(query)
}

Rappel de requête

Les API suivantes pour configurer les rappels de requête ne sont pas disponibles en commun et ne sont donc pas disponibles sur d'autres plates-formes qu'Android.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

Nous prévoyons d'ajouter la prise en charge du rappel de requête dans une prochaine version de Room.

L'API permettant de configurer un RoomDatabase avec un rappel de requête RoomDatabase.Builder.setQueryCallback ainsi que l'interface de rappel RoomDatabase.QueryCallback ne sont pas disponibles en commun et ne sont donc pas disponibles sur d'autres plates-formes que Android.

Base de données à fermeture automatique

L'API permettant d'activer la fermeture automatique après un délai avant expiration, RoomDatabase.Builder.setAutoCloseTimeout, n'est disponible que sur Android et non sur d'autres plates-formes.

Pré-packager la base de données

Les API suivantes permettant de créer un RoomDatabase à l'aide d'une base de données existante (c'est-à-dire une base de données préempaquetée) ne sont pas disponibles en commun et ne sont donc pas disponibles sur d'autres plates-formes que Android. Voici les API concernées:

  • RoomDatabase.Builder.createFromAsset
  • RoomDatabase.Builder.createFromFile
  • RoomDatabase.Builder.createFromInputStream
  • RoomDatabase.PrepackagedDatabaseCallback

Nous prévoyons d'ajouter la compatibilité avec les bases de données préemballées dans une prochaine version de Room.

Invalidation multi-instance

L'API permettant d'activer l'invalidation multi-instance, RoomDatabase.Builder.enableMultiInstanceInvalidation, n'est disponible que sur Android et n'est pas disponible sur les plates-formes courantes ni sur d'autres plates-formes.