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.

Set up dependencies

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

Define the dependencies in the libs.versions.toml file:

[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" }

Add the Room Gradle Plugin to configure Room schemas and the KSP plugin

plugins {
  alias(libs.plugins.ksp)
  alias(libs.plugins.androidx.room)
}

Add the Room runtime dependency and the bundled SQLite library:

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

Add the KSP dependencies to the root dependencies block. Note that you need to add all the targets your app uses. For more information, check KSP with 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
}

Define the Room schema directory. For additional information, see Set schema location using Room Gradle Plugin.

room {
    schemaDirectory("$projectDir/schemas")
}

Define 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.

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

When you declare an expect object with the interface RoomDatabaseConstructor, the Room compiler generates the actual implementations. Android Studio might issue the following warning, which you can suppress with @Suppress("KotlinNoActualForExpect"):

Expected object 'AppDatabaseConstructor' has no actual declaration in module`

Next, either define a new DAO interface or move an existing one to 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>>
}

Define or move your entities to commonMain:

// shared/src/commonMain/kotlin/TodoEntity.kt

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

Create the platform-specific 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.

Android

On Android, database location is usually obtained through the Context.getDatabasePath() API. To create the database instance, specify a Context along with the database path.

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

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

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

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

Instantiate the database

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
      .setDriver(BundledSQLiteDriver())
      .setQueryCoroutineContext(Dispatchers.IO)
      .build()
}

Select a SQLite driver

The previous code snippet calls the setDriver builder function to define what SQLite driver the Room database should use. These drivers differ based on the target platform. 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 want to use the OS-provided SQLite, use the setDriver API in the platform-specific source sets that specify a platform-specific driver. See Driver implementations for descriptions of available driver implementations. You can use either of the following:

To use NativeSQLiteDriver, you need to provide a linker option -lsqlite3 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")
        }
    }
}

Set a Coroutine context (Optional)

A RoomDatabase object on Android can optionally be configured with shared application executors using RoomDatabase.Builder.setQueryExecutor() to perform database operations.

Because executors are not KMP compatible, Room's setQueryExecutor() API is not available in commonMain. Instead the RoomDatabase object must be configured with a CoroutineContext, which can be set using RoomDatabase.Builder.setCoroutineContext(). If no context is set, then the RoomDatabase object will default to using Dispatchers.IO.

Minification and obfuscation

If the project is minified or obfuscated then you must include the following ProGuard rule so that Room can find the generated implementation of the database definition:

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

Migrate to Kotlin Multiplatform

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.

Migrate from Support SQLite to SQLite Driver

Any usages of SupportSQLiteDatabase and other APIs in androidx.sqlite.db need to be refactored with SQLite Driver APIs, because the APIs in androidx.sqlite.db are Android-only (note the different package than the KMP package).

For backwards compatibility, and as long as the RoomDatabase is configured with a SupportSQLiteOpenHelper.Factory (for example, no SQLiteDriver is set), then Room behaves in 'compatibility mode' where both Support SQLite and SQLite Driver APIs work as expected. This enables incremental migrations so that you don't need to convert all your Support SQLite usages to SQLite Driver in a single change.

Convert Migrations Subclasses

Migrations subclasses need to be migrated to the SQLite driver counterparts:

Kotlin Multiplatform

Migration subclasses

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(connection: SQLiteConnection) {
    // …
  }
}

Auto migration specification subclasses

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(connection: SQLiteConnection) {
    // …
  }
}

Android-only

Migration subclasses

object Migration_1_2 : Migration(1, 2) {
  override fun migrate(db: SupportSQLiteDatabase) {
    // …
  }
}

Auto migration specification subclasses

class AutoMigrationSpec_1_2 : AutoMigrationSpec {
  override fun onPostMigrate(db: SupportSQLiteDatabase) {
    // …
  }
}

Convert database callback

Database callbacks need to be migrated to the SQLite driver counterparts:

Kotlin Multiplatform

object MyRoomCallback : RoomDatabase.Callback() {
  override fun onCreate(connection: SQLiteConnection) {
    // …
  }

  override fun onDestructiveMigration(connection: SQLiteConnection) {
    // …
  }

  override fun onOpen(connection: SQLiteConnection) {
    // …
  }
}

Android-only

object MyRoomCallback : RoomDatabase.Callback() {
  override fun onCreate(db: SupportSQLiteDatabase) {
    // …
  }

  override fun onDestructiveMigration(db: SupportSQLiteDatabase) {
    // …
  }

  override fun onOpen(db: SupportSQLiteDatabase) {
    // …
  }
}

Convert @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.

Kotlin Multiplatform

Define the raw query

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

Android-only

Define the raw query

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

A SimpleSQLiteQuery can then be used to create a query at 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)
}

Convert blocking DAO functions

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 DAOs implemented in androidMain to maintain backwards compatibility with the existing codebase. When using Room for KMP, all DAO functions compiled for non-Android platforms need to be suspend functions.

Kotlin Multiplatform

Suspending queries

@Query("SELECT * FROM Todo")
suspend fun getAllTodos(): List<Todo>

Suspending transactions

@Transaction
suspend fun transaction() {  }

Android-only

Blocking queries

@Query("SELECT * FROM Todo")
fun getAllTodos(): List<Todo>

Blocking transactions

@Transaction
fun blockingTransaction() {  }

Convert reactive types to Flow

Not all DAO functions need to be suspend functions. DAO functions that return reactive types such as LiveData or RxJava's Flowable shouldn't be converted to suspend functions. Some types, however, such as LiveData are not KMP compatible. DAO functions with reactive return types must be migrated to coroutine flows.

Kotlin Multiplatform

Reactive types Flows

@Query("SELECT * FROM Todo")
fun getTodosFlow(): Flow<List<Todo>>

Android-only

Reactive types like LiveData or RxJava's Flowable

@Query("SELECT * FROM Todo")
fun getTodosLiveData(): LiveData<List<Todo>>

Convert transaction APIs

Database transaction APIs for Room KMP can differentiate between writing (useWriterConnection) and reading (useReaderConnection) transactions.

Kotlin Multiplatform

val database: RoomDatabase = 
database.useWriterConnection { transactor ->
  transactor.immediateTransaction {
    // perform database operations in transaction
  }
}

Android-only

val database: RoomDatabase = 
database.withTransaction {
  // perform database operations in transaction
}

Write transactions

Use write transactions to make sure that multiple queries write data atomically, so that readers can consistently access the data. You can do this using useWriterConnection with any of the three transaction types:

  • immediateTransaction: In Write-Ahead Logging (WAL) mode (default), this type of transaction acquires a lock when it starts, but readers can continue to read. This is the preferred choice for most cases.

  • deferredTransaction: The transaction won't acquire a lock until the first write statement. Use this type of transaction as an optimization when you're not sure if a write operation will be needed within the transaction. For example, if you start a transaction to delete songs from a playlist given just a name of the playlist and the playlist doesn't exist, then no write (delete) operation is needed.

  • exclusiveTransaction: This mode behaves identical to immediateTransaction in the WAL mode. In other journaling modes, it prevents other database connections from reading the database while the transaction is underway.

Read transactions

Use read transactions to consistently read from the database multiple times. For example, when you have two or more separate queries and you don't use a JOIN clause. Only deferred transactions are allowed in reader connections. Attempting to start an immediate or exclusive transaction in a reader connection will throw an exception, as these are considered 'write' operations.

val database: RoomDatabase = 
database.useReaderConnection { transactor ->
  transactor.deferredTransaction {
      // perform database operations in transaction
  }
}

Not Available in Kotlin Multiplatform

Some of the APIs that were available for Android are not available in Kotlin Multiplatform.

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.