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