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 module:
androidx.room:room-gradle-plugin
- The Gradle Plugin to configure Room schemasandroidx.room:room-compiler
- The KSP processor that generates codeandroidx.room:room-runtime
- The runtime part of the libraryandroidx.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:
- Set schema location using Room Gradle Plugin.
- KSP with Kotlin Multiplatform.
- Adding runtime dependencies.
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, ) }
Minification and obfuscation
If the project is minified or obfuscated then the following proguard rule must be included so that Room can find the generated implementation of the database definition:
-keep class * extends androidx.room.RoomDatabase { <init>(); }
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.