For years, SQLDelight was the go-to choice for relational database persistence in Kotlin Multiplatform (KMP) applications. While SQLDelight is a fantastic tool, many developers coming from native Android development missed the ease-of-use, familiar annotation-based API, and powerful relationship mapping of Room.
Recognizing this need, Google expanded its Jetpack Room library to support Kotlin Multiplatform natively starting in Room 2.7.0. Now, you can use the same @Database, @Dao, and @Entity annotations in your shared KMP logic.
Here is a step-by-step guide to setting up and using Room KMP in a shared Kotlin Multiplatform codebase.
Why Choose Room KMP?
- Familiar API: If you already know Room for Android, the learning curve is zero.
- KSP Compile-time Safety: Just like on Android, query validation and schema compile-time checks are performed by Kotlin Symbol Processing (KSP).
- Auto-Migrations: Room handles database schema migrations automatically or via simple programmatic callbacks.
- Structured Relations: Multi-table relationships can be modeled easily with
@Relationand@Embedded.
1. Project Configuration & Dependencies
Room KMP requires KSP (Kotlin Symbol Processing). Ensure the KSP plugin is applied in your project.
In your shared module's build.gradle.kts:
plugins {
kotlin("multiplatform")
id("com.google.devtools.ksp") version "2.0.0-1.0.22" // Match your Kotlin version
id("androidx.room") version "2.7.0-alpha05" // Or latest stable version
}
room {
schemaDirectory("$projectDir/schemas")
}
kotlin {
// Platform targets (android, ios, etc.)
androidTarget()
iosArm64()
iosSimulatorArm64()
iosX64()
sourceSets {
commonMain.dependencies {
implementation("androidx.room:room-runtime:2.7.0-alpha05")
implementation("androidx.sqlite:sqlite-bundled:2.5.0-alpha05") // Bundled SQLite driver
}
}
}
dependencies {
// Configure Room compiler via KSP for the shared module
add("kspCommonMainMetadata", "androidx.room:room-compiler:2.7.0-alpha05")
}
2. Defining Entities and DAOs
Just like on Android, you define your schema using annotations inside src/commonMain/kotlin/.
Let's define a simple TaskEntity:
// commonMain/.../data/TaskEntity.kt
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class TaskEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val description: String,
val isCompleted: Boolean
)
Now, define the Data Access Object (DAO):
// commonMain/.../data/TaskDao.kt
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface TaskDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTask(task: TaskEntity)
@Query("SELECT * FROM TaskEntity")
fun getAllTasks(): Flow<List<TaskEntity>>
@Query("DELETE FROM TaskEntity WHERE id = :id")
suspend fun deleteTaskById(id: Long)
}
3. Creating the Room Database
To define your database, you need to create an abstract class that extends RoomDatabase.
In Room KMP, we also need to leverage a helper class RoomDatabaseConstructor so Room's generated compiler code can instantiate our database in the multiplatform environment.
// commonMain/.../data/AppDatabase.kt
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.ConstructedBy
import androidx.room.RoomDatabaseConstructor
@Database(entities = [TaskEntity::class], version = 1)
@ConstructedBy(AppDatabaseConstructor::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun taskDao(): TaskDao
}
// Expect constructor definition for KSP compile-time generation
@Suppress("NO_ACTUAL_FOR_EXPECT")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
override fun initialize(): AppDatabase
}
4. Platform-Specific Database Builders
To instantiate the database on Android and iOS, we need to supply the platform-specific path and driver initialization using expect/actual.
In commonMain:
// commonMain/.../data/DatabaseBuilder.kt
import androidx.room.RoomDatabase
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
expect fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase>
fun getRoomDatabase(builder: RoomDatabase.Builder<AppDatabase>): AppDatabase {
return builder
.setDriver(BundledSQLiteDriver()) // Use high-performance bundled SQLite
.fallbackToDestructiveMigration(dropAllTables = true)
.build()
}
Now, implement the actual builders on Android and iOS.
On Android, we need the application Context to determine the database file path:
// androidMain/.../data/DatabaseBuilder.android.kt
import android.content.Context
import androidx.room.Room
import androidx.room.RoomDatabase
import java.io.File
lateinit var appContext: Context
actual fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
val dbFile = appContext.getDatabasePath("app_tasks.db")
return Room.databaseBuilder<AppDatabase>(
context = appContext,
name = dbFile.absolutePath
)
}
On iOS, we can save the database file inside the App's standard NSDocumentDirectory:
// iosMain/.../data/DatabaseBuilder.ios.kt
import androidx.room.Room
import androidx.room.RoomDatabase
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSFileManager
import platform.Foundation.NSUserDomainMask
actual fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
directory = NSDocumentDirectory,
inDomain = NSUserDomainMask,
appropriateForURL = null,
create = true,
error = null
)
val dbFilePath = documentDirectory?.path + "/app_tasks.db"
return Room.databaseBuilder<AppDatabase>(
name = dbFilePath,
factory = { AppDatabaseConstructor.initialize() }
)
}
5. Integrating with the Repository Layer
Now we can inject or construct our database inside a unified repository in commonMain and access our flows:
// commonMain/.../data/TaskRepository.kt
import kotlinx.coroutines.flow.Flow
class TaskRepository(private val database: AppDatabase) {
private val taskDao = database.taskDao()
fun getTasks(): Flow<List<TaskEntity>> {
return taskDao.getAllTasks()
}
suspend fun saveTask(title: String, description: String) {
taskDao.insertTask(
TaskEntity(
title = title,
description = description,
isCompleted = false
)
)
}
suspend fun deleteTask(id: Long) {
taskDao.deleteTaskById(id)
}
}
This repository can then be shared with Compose Multiplatform views or native iOS Swift code using standard dependency injection (like Koin).
Conclusion
Room KMP is an incredibly welcome addition to the Kotlin Multiplatform toolchain. It brings Android’s most popular ORM straight into shared mobile architecture, providing compile-time safe SQL queries, automated migrations, and easy entity relationship modeling without sacrificing native iOS execution.
If you are looking to build modern, offline-first mobile products, Room KMP is a first-class choice for database storage.
For setting up network integration alongside your Room database, check out my guide on Setting Up Ktor for Networking in Kotlin Multiplatform.
Hire a Kotlin Multiplatform Specialist
Need support setting up offline-first storage, integrating KSP, or designing clean architecture for your Android and iOS applications? I provide remote contract and consulting services for engineering teams in Europe, the UK, and Canada with flexible timezone alignment (CEST, GMT, and EST).