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 set up 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]
room3 = "3.0.1"
sqlite = "2.7.0"
ksp = "<kotlinCompatibleKspVersion>"
[libraries]
androidx-sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
androidx-room3-runtime = { module = "androidx.room3:room3-runtime", version.ref = "room3" }
androidx-room3-compiler = { module = "androidx.room3:room3-compiler", version.ref = "room3" }
# Optional SQLite Wrapper
androidx-room3-sqlite-wrapper = { module = "androidx.room3:room3-sqlite-wrapper", version.ref = "room3" }
[plugins]
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
androidx-room3 = { id = "androidx.room3", version.ref = "room3" }
Add the Room Gradle Plugin to configure Room schemas and the KSP plugin.
plugins {
alias(libs.plugins.ksp)
alias(libs.plugins.androidx.room3)
}
Add the Room runtime dependency and the bundled SQLite library:
commonMain.dependencies {
implementation(libs.androidx.room3.runtime)
implementation(libs.androidx.sqlite.bundled)
}
// Optional when using Room SQLite Wrapper
androidMain.dependencies {
implementation(libs.androidx.room3.sqlite.wrapper)
}
Add the KSP dependencies to the root dependencies block. You need to add
all the targets your app uses. For more information, see KSP with
Kotlin Multiplatform.
dependencies {
add("kspAndroid", libs.androidx.room3.compiler)
add("kspIosSimulatorArm64", libs.androidx.room3.compiler)
add("kspIosX64", libs.androidx.room3.compiler)
add("kspIosArm64", libs.androidx.room3.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 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
RoomDatabaseConstructor interface, 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 platform-specific database builders
You need to define a database builder to instantiate Room on each platform. This is the only part of the API you must define in platform-specific source sets, due to differences in file-system APIs.
Android
On Android, you usually obtain the database location using the
Context.getDatabasePath API. To create the database instance, specify a
Context and 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.
It's 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:
AndroidSQLiteDriverinandroidMainNativeSQLiteDriveriniosMain
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
A RoomDatabase object on Android can optionally be configured with shared
application executors using RoomDatabase.Builder.setQueryExecutor to perform
database operations.
Because executors aren't KMP compatible, Room's setQueryExecutor API isn't
available in commonMain. Instead, the RoomDatabase object must be
configured with a CoroutineContext, which you can set using
RoomDatabase.Builder.setCoroutineContext. If you don't set a context, the
RoomDatabase object defaults to 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.room3.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.
Use Room SQLite wrapper
The androidx.room3:room3-sqlite-wrapper artifact provides APIs to bridge
between SQLiteDriver and SupportSQLiteDatabase during migration.
To get a SupportSQLiteDatabase from a RoomDatabase configured with a
SQLiteDriver, use the new extension function
RoomDatabase.getSupportWrapper. This compatibility wrapper helps maintain
existing usages of SupportSQLiteDatabase, which you often obtain from
RoomDatabase.openHelper.writableDatabase, while adopting SQLiteDriver,
especially for codebases with extensive SupportSQLite API usages that want to
use BundledSQLiteDriver.
Convert migration 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 toimmediateTransactionin 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.setQueryCallbackRoomDatabase.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-packaged 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.createFromAssetRoomDatabase.Builder.createFromFileRoomDatabase.Builder.createFromInputStreamRoomDatabase.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.
Recommended for you
- Note: link text is displayed when JavaScript is off
- Migrate existing apps to Room KMP Codelab
- Get Started with KMP Codelab
- Save data in local database with Room