Room (Kotlin Multiplatform)

The Room persistence library provides an abstraction layer over SQLite to allow for more robust database access while harnessing the full power of SQLite. This page focuses on using Room in Kotlin Multiplatform (KMP) projects. For more information on using Room, see Save data in a local database using Room or our official samples.

Setting up dependencies

The current version of Room that supports KMP is 2.7.0-alpha01 or higher.

To setup Room in your KMP project, add the dependencies for the artifacts in the build.gradle.kts file for your module:

  • androidx.room:room-gradle-plugin - The Gradle Plugin to configure Room schemas
  • androidx.room:room-compiler - The KSP processor that generates code
  • androidx.room:room-runtime - The runtime part of the library
  • androidx.sqlite:sqlite-bundled - (Optional) The bundled SQLite library

Additionally you need to configure Room's SQLite driver. These drivers differ based on the target platform. See Driver implementations for descriptions of available driver implementations.

For additional setup information, see the following:

Defining the database classes

You need to create a database class annotated with @Database along with DAOs and entities inside the common source set of your shared KMP module. Placing these classes in common sources will allow them to be shared across all target platforms.

When you declare an expect object with the interface RoomDatabaseConstructor, the Room compiler generates the actual implementations. Android Studio might issue a warning "Expected object 'AppDatabaseConstructor' has no actual declaration in module"; you can suppress the warning with @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
)

Note that you can optionally use actual / expect declarations to create platform-specific Room implementations. For example, you can add a platform-specific DAO that is defined in common code using expect and then specify the actual definitions with additional queries in platform-specific source sets.

Creating the database builder

You need to define a database builder to instantiate Room on each platform. This is the only part of the API that is required to be in platform-specific source sets due to the differences in file system APIs. For example, in Android, database location is usually obtained through the Context.getDatabasePath() API, while for iOS, the database location is be obtained using NSFileManager.

Android

To create the database instance, specify a Context along with the database path.

// 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

To create the database instance, provide a database path using the NSFileManager, usually located in the 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 (Desktop)

To create the database instance, provide a database path using Java or Kotlin APIs.

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

Database instantiation

Once you obtain the RoomDatabase.Builder from one of the platform-specific constructors, you can configure the rest of the Room database in common code along with the actual database instantiation.

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

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

Selecting a SQLiteDriver

The previous code snippets use the BundledSQLiteDriver. This is the recommended driver that includes SQLite compiled from source, which provides the most consistent and up-to-date version of SQLite across all platforms. If you wish to use the OS-provided SQLite, use the setDriver API in platform-specific source sets that specify a platform-specific driver. For Android, you can use AndroidSQLiteDriver, while for iOS you can use the NativeSQLiteDriver. To use NativeSQLiteDriver, you need to provide a linker option so that the iOS app dynamically links with the system SQLite.

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

Differences

Room was originally developed as an Android library and was later migrated to KMP with a focus on API compatibility. The KMP version of Room differs somewhat between platforms and from the Android-specific version. These differences are listed and described as follows.

Blocking DAO functions

When using Room for KMP, all DAO functions compiled for non-Android platforms need to be suspend functions with the exception of reactive return types, such as 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 benefits from the feature-rich asynchronous kotlinx.coroutines library that Kotlin offers for multiple platforms. For optimal functionality, suspend functions are enforced for DAOs compiled in a KMP project, with the exception of Android specific DAOs to maintain backwards compatibility with the existing codebase.

Feature differences with KMP

This section describes how features differ between KMP and Android platform versions of Room.

@RawQuery DAO functions

Functions annotated with @RawQuery that are compiled for non-Android platforms will need to declare a parameter of type RoomRawQuery instead of SupportSQLiteQuery.

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

A RoomRawQuery can then be used to create a query at runtime:

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

Query Callback

The following APIs for configuring query callbacks are not available in common and are thus unavailable in platforms other than Android.

  • RoomDatabase.Builder.setQueryCallback
  • RoomDatabase.QueryCallback

We intend to add support for query callback in a future version of Room.

The API to configure a RoomDatabase with a query callback RoomDatabase.Builder.setQueryCallback along with the callback interface RoomDatabase.QueryCallback are not available in common and thus not available in other platforms other than Android.

Auto Closing Database

The API to enable auto-closing after a timeout, RoomDatabase.Builder.setAutoCloseTimeout, is only available on Android and is not available in other platforms.

Pre-package Database

The following APIs to create a RoomDatabase using an existing database (i.e. a pre-packaged database) are not available in common and are thus not available in other platforms other than Android. These APIs are:

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

We intend to add support for pre-packaged databases in a future version of Room.

Multi-Instance Invalidation

The API to enable multi-instance invalidation, RoomDatabase.Builder.enableMultiInstanceInvalidation is only available on Android and is not available in common or other platforms.