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.8.2" sqlite = "2.6.1" 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:
AndroidSQLiteDriver
inandroidMain
NativeSQLiteDriver
iniosMain
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.
Use Room SQLite Wrapper (Optional)
The androidx.room:room-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
(often obtained from RoomDatabase.openHelper.writableDatabase
) while adopting SQLiteDriver
, especially for codebases with extensive SupportSQLite
API usages that want to use BundledSQLiteDriver
.
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 toimmediateTransaction
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.
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