Skip to content

Does Jetpack Room Support Desktop JVM? Room 3.0 Desktop Target Explained

I was building a Kotlin Multiplatform project and needed to share database code between Android and Desktop JVM targets. The question that kept coming up: Does Jetpack Room support Desktop JVM?

The Problem: Cross-Platform Database Needs

When I started my Kotlin Multiplatform project, I wanted to share as much code as possible between platforms. Database logic seemed like the perfect candidate for code sharing. But Room was historically Android-only, and I kept seeing conflicting information about desktop support.

The alternatives looked less appealing:

  • SQLDelight requires learning a different query syntax
  • Exposed is JVM-only (no iOS support)
  • Writing platform-specific database code defeats the purpose of KMP

I needed a definitive answer about Room’s desktop capabilities.

The Answer: Yes, Room Supports Desktop JVM

After digging through documentation and community discussions, I found that Room supports Desktop JVM as a Kotlin Multiplatform target. This support existed before Room 3.0.

The Room 3.0 alpha01 announcement caused confusion because it highlighted JS and WasmJS targets:

Room 3.0 alpha01 adds:
├─ JS target (JavaScript)
├─ WasmJS target (WebAssembly)
└─ Desktop JVM was already supported

When developers asked “what about DesktopJVM?” in the Reddit discussion, the community confirmed: “It already support” - meaning Desktop JVM support was available before Room 3.0.

How Room KMP Targets Work

Room now supports five targets with a single database definition:

TargetStatusUse Case
Android JVMStableAndroid apps
Desktop JVMStableDesktop applications
iOS (via Kotlin/Native)StableiOS apps
JSAlpha (3.0+)Web browsers
WasmJSAlpha (3.0+)WebAssembly

This means you can write your database layer once in commonMain and use it everywhere.

Implementation: Shared Database Definition

I’ll show you how to set up Room for cross-platform use. The key is placing your database code in commonMain and platform-specific initialization in source sets.

Step 1: Define Entities in commonMain

commonMain/kotlin/User.kt
@Entity
data class User(
@PrimaryKey val id: Long,
val name: String,
val email: String
)

Step 2: Define DAOs in commonMain

commonMain/kotlin/UserDao.kt
@Dao
interface UserDao {
@Query("SELECT * FROM User")
suspend fun getAll(): List<User>
@Insert
suspend fun insert(user: User)
@Delete
suspend fun delete(user: User)
}

Step 3: Define Database in commonMain

commonMain/kotlin/AppDatabase.kt
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}

This single database definition works across Android, Desktop, iOS, JS, and WasmJS.

Platform-Specific Initialization

The database builder needs platform-specific paths. Here’s how I handle initialization for each platform.

Desktop JVM Initialization

jvmMain/kotlin/DatabaseFactory.kt
import java.io.File
fun createDatabase(): AppDatabase {
val dbFile = File(System.getProperty("user.home"), ".myapp/database.db")
// Ensure parent directory exists
dbFile.parentFile?.mkdirs()
return Room.databaseBuilder<AppDatabase>(
AppDatabase::class.java,
dbFile.absolutePath
).build()
}

The desktop version uses System.getProperty("user.home") to get the user’s home directory, which is the standard location for application data on desktop platforms.

Android Initialization

androidMain/kotlin/DatabaseFactory.kt
import android.content.Context
fun createDatabase(context: Context): AppDatabase {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app-database"
).build()
}

Android uses Context to access the app’s database directory, which is handled automatically by the framework.

iOS Initialization

iosMain/kotlin/DatabaseFactory.kt
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSFileManager
import platform.Foundation.NSUserDomainMask
fun createDatabase(): AppDatabase {
val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
NSDocumentDirectory,
NSUserDomainMask,
null,
false,
null
)!!
return Room.databaseBuilder<AppDatabase>(
AppDatabase::class.java,
documentDirectory.path + "/app-database.db"
).build()
}

iOS requires accessing the documents directory through NSFileManager, which provides the appropriate sandboxed storage location.

Why This Matters

I’ve identified several key benefits of using Room for cross-platform database needs:

Code Reuse Across Platforms

Traditional approach:
├─ Android: Room implementation (500 lines)
├─ Desktop: Exposed or raw SQLite (500 lines)
├─ iOS: Core Data or SQLite.swift (500 lines)
└─ Total: 1500 lines of duplicated logic
Room KMP approach:
├─ commonMain: Shared database logic (500 lines)
├─ Platform initialization: 30 lines each
└─ Total: 590 lines with 60% reduction

Consistency in ORM Patterns

The same Room patterns I use on Android work everywhere:

  • Entity annotations for data classes
  • DAO interfaces for data access
  • Migration strategies
  • Type converters

No need to learn different ORM paradigms for each platform.

Compile-Time Verification

Room’s compile-time checks work on desktop too. I get caught errors before runtime:

Compile-time error example
@Dao
interface UserDao {
@Query("SELECT * FROM NonExistentTable") // Error at compile time
suspend fun getInvalid(): List<User>
}

The compiler validates:

  • SQL syntax
  • Table and column names
  • Type mappings
  • Query return types

Common Mistakes to Avoid

When implementing Room for desktop, I made several mistakes that you should avoid.

Mistake 1: Using Android-Specific APIs in commonMain

Wrong - commonMain
// WRONG: Context is Android-only
fun createDatabase(context: Context): AppDatabase {
// This won't compile for desktop/iOS
}

Solution: Keep platform-specific types in platform source sets:

Correct - expect/actual pattern
// commonMain
expect fun createDatabase(): AppDatabase
// androidMain
actual fun createDatabase(): AppDatabase {
val context = getAndroidContext() // Platform-specific
return Room.databaseBuilder(context, ...)
}
// jvmMain (desktop)
actual fun createDatabase(): AppDatabase {
val path = System.getProperty("user.home") + "/.app/db"
return Room.databaseBuilder(AppDatabase::class.java, path)
}

Mistake 2: Incorrect Desktop Database Paths

Wrong path handling
// WRONG: Relative path might resolve to unexpected location
val db = Room.databaseBuilder(
AppDatabase::class.java,
"database.db" // Where is this?
).build()

Solution: Always use absolute paths with proper directory creation:

Correct path handling
val dbFile = File(System.getProperty("user.home"), ".myapp/database.db")
dbFile.parentFile?.mkdirs() // Create directory if needed
val db = Room.databaseBuilder(
AppDatabase::class.java,
dbFile.absolutePath
).build()

Mistake 3: Assuming All Features Work Identically

Some Room features behave differently on desktop:

Android vs Desktop Differences:
├─ FTS (Full-Text Search): Works differently on SQLite versions
├─ Migrations: Need to handle SQLite version differences
├─ File permissions: Desktop has more flexibility
└─ Background threads: Threading models differ

Project Structure

Here’s how I organize a Kotlin Multiplatform project with Room:

my-project/
├─ shared/
│ ├─ commonMain/kotlin/
│ │ ├─ data/
│ │ │ ├─ User.kt (Entity)
│ │ │ ├─ UserDao.kt (DAO)
│ │ │ └─ AppDatabase.kt (Database)
│ │ └─ DatabaseFactory.kt (expect)
│ ├─ androidMain/kotlin/
│ │ └─ DatabaseFactory.kt (actual with Context)
│ ├─ jvmMain/kotlin/
│ │ └─ DatabaseFactory.kt (actual for desktop)
│ └─ iosMain/kotlin/
│ └─ DatabaseFactory.kt (actual for iOS)
├─ androidApp/ (Android application)
├─ desktopApp/ (Desktop JVM application)
└─ iosApp/ (iOS application)

Gradle Configuration

Setting up Room in a Kotlin Multiplatform project requires specific Gradle configuration:

build.gradle.kts
kotlin {
androidTarget()
jvm("desktop") // Desktop JVM target
iosX64()
iosArm64()
sourceSets {
val commonMain by getting {
dependencies {
implementation("androidx.room:room-runtime:2.6.1")
implementation("androidx.room:room-ktx:2.6.1")
}
}
val androidMain by getting {
dependencies {
implementation("androidx.room:room-runtime:2.6.1")
}
}
val desktopMain by getting {
dependencies {
implementation("androidx.room:room-runtime:2.6.1")
// SQLite JDBC for desktop
implementation("org.xerial:sqlite-jdbc:3.42.0.0")
}
}
}
}

Migration Strategy

If you’re migrating from Android-only Room to cross-platform Room:

Step 1: Extract to commonMain

Move your entities, DAOs, and database class to commonMain:

Before: Android-only location
// androidApp/src/main/kotlin/data/User.kt
@Entity
data class User(...)
// Move to:
// shared/commonMain/kotlin/data/User.kt

Step 2: Create Platform-Specific Factories

Use the expect/actual pattern for database creation:

commonMain
expect fun createDatabaseBuilder(): RoomDatabase.Builder<AppDatabase>
androidMain
actual fun createDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
return Room.databaseBuilder(
context,
AppDatabase::class.java,
"app-database"
)
}
jvmMain
actual fun createDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
val path = File(System.getProperty("user.home"), ".app/db").absolutePath
return Room.databaseBuilder(
AppDatabase::class.java,
path
)
}

Step 3: Update Gradle Configuration

Add Kotlin Multiplatform plugin and configure targets.

Performance Considerations

I noticed performance differences between platforms:

PlatformDatabase LocationTypical PerformanceNotes
AndroidInternal storageSlower (encrypted FS)Battery considerations
DesktopUser home directoryFastSSD improves significantly
iOSApp sandboxMediumiCloud backup enabled by default

For desktop applications, I recommend:

  1. Use SSD storage for database files
  2. Enable WAL mode for better concurrent access
  3. Implement connection pooling for multi-threaded access
Enable WAL mode
val db = Room.databaseBuilder<AppDatabase>(...)
.setJournalMode(JournalMode.WRITE_AHEAD_LOGGING)
.build()

Testing Cross-Platform Database Code

Testing Room database code across platforms requires platform-specific test setups:

commonTest
class UserDaoTest {
@Test
fun testInsertAndRetrieve() = runTest {
val db = createTestDatabase()
val dao = db.userDao()
val user = User(1, "Test", "[email protected]")
dao.insert(user)
val retrieved = dao.getAll()
assertEquals(1, retrieved.size)
assertEquals("Test", retrieved[0].name)
}
}

Each platform provides its own test database factory:

androidTest
actual fun createTestDatabase(): AppDatabase {
return Room.inMemoryDatabaseBuilder(
context,
AppDatabase::class.java
).build()
}
desktopTest
actual fun createTestDatabase(): AppDatabase {
return Room.inMemoryDatabaseBuilder(
AppDatabase::class.java
).build()
}

Summary

Jetpack Room supports Desktop JVM through Kotlin Multiplatform, allowing you to share database code between Android, desktop, iOS, and web applications. The key points:

  1. Desktop JVM support exists - Available before Room 3.0
  2. Room 3.0 adds JS and WasmJS - Expanding to web targets
  3. Single database definition - Works across all platforms
  4. Platform-specific initialization - Handle file paths per platform
  5. Avoid Android-specific APIs - Keep commonMain platform-agnostic

The days of writing separate database layers for each platform are over. With Room’s Kotlin Multiplatform support, I can now share my data layer between Android and desktop applications with minimal platform-specific code.

Final Words + More Resources

My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me

Here are also the most important links from this article along with some further resources that will help you in this scope:

Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!

Comments