top of page
Search

Using Room Database in Compose Multiplatform (KMP/CMP): A Step-by-Step Guide- by Nimesh Vasani

  • Writer: nimesh Vasani
    nimesh Vasani
  • Feb 12
  • 4 min read

Updated: Feb 13

Room Database is now officially supported in Compose Multiplatform (KMP/CMP), making it easier to manage structured data across Android, iOS, and desktop platforms. In this guide, we'll explore how to integrate Room Database into a Compose Multiplatform project, covering setup, entity creation, DAO implementation, and database initialization.



Setting up dependencies ⚙️🔗


The current version of Room that supports KMP is 2.7.0-alpha01 or higher.

Open libs.version.toml file and add following dependencies 

First add version variables, You should check for latest version.

[versions]

room = "2.7.0-alpha13"
ksp = "2.1.0-1.0.29"
sqlite = "2.5.0-SNAPSHOT"
roomRuntimeJvm = "2.7.0-alpha13"

Then add followings to Library Section

[libraries]

room-runtime = { module = "androidx.room:room-runtime", version.ref = "room" }
room-runtime-android = { module = "androidx.room:room-runtime-android", version.ref = "room" }
room-compiler = { module = "androidx.room:room-compiler", version.ref = "room" }
sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
androidx-room-runtime-jvm = { group = "androidx.room", name = "room-runtime-jvm", version.ref = "roomRuntimeJvm" }

Last for plugins

[plugins]

ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
room = { id = "androidx.room", version.ref = "room" }

Open app level gradle file, Build.Gradle.kts 

Then add following dependency: 💉


First of all in Plugin section add plugins for Room and Ksp


plugins {
    alias(libs.plugins.ksp)
    alias(libs.plugins.room)
}

Then scroll to SourceSets, and add dependencies to commonMain.dependencies block


  commonMain.dependencies {
 			implementation(libs.room.runtime)
            implementation(libs.sqlite.bundled)
}

For schema for Room database you need to specify manually, for that scroll down add room{} block and add following dependency


room {
    schemaDirectory("$projectDir/schemas")
}

Last you will see another dependency section, which may have default UITooling dependecy there, add Ksp compiler dependency there.


dependencies {
    ksp(libs.room.compiler)
    debugImplementation(compose.uiTooling)
}

Sync the gradle file after adding all dependencies.


Note : - You may notice some red lines, indicating errors while adding dependencies, ignore it until build finish, if errors try clean project and rebuild.



Create database classes 🗄️


To enable seamless database access across all target platforms in your Compose Multiplatform project, you should define your database class, annotated with @Database, along with the necessary DAOs and entities inside the common source set of your shared KMP module.


By placing these classes in the common source set, they become accessible to all platforms, ensuring code reusability.


When defining an expect object implementing the RoomDatabaseConstructor interface, the Room compiler will generate the corresponding actual implementations. However, Android Studio may display a warning: "Expected object 'AppDatabaseConstructor' has no actual declaration in module." You can suppress this warning using @Suppress("NO_ACTUAL_FOR_EXPECT").


As an Android Developer, I am expecting my readers know about basics of Room Database.

So Let's focus more on KMP-Room set up rather than actual Room database work flow.

Here, I am using User class as data class which will be work as Table/Entity in our room database. If you have complex data structure in your table, use Convertors to store complex object.



Create package db in common/shared module create models package, then create kotlin data class "user.kt"

//add your package hierarchy
 
import androidx.room.Entity
import androidx.room.PrimaryKey


@Entity(tableName = "users")

data class User(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val name: String,
    val age: Int,
    val hobbies: List<String> // Room does not support lists 				  directly, so we need a converter
)

Create Database access object first, annotate with Dao, create file "UserDao.kt"


import androidx.room.*

@Dao
interface UserDao {
    @Insert(onConflict = OnConflictStrategy.REPLACE)
    suspend fun insertUser(user: User)

    @Query("SELECT * FROM users")
    suspend fun getAllUsers(): List<User>

    @Delete
    suspend fun deleteUser(user: User)
}

Now let's Create a new file in database package. "AppDatabase.kt" 📝


package ...

import androidx.room.ConstructedBy
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.RoomDatabaseConstructor
import androidx.room.TypeConverters
import androidx.sqlite.driver.bundled.BundledSQLiteDriver


@Database(entities = [User::class], version = 1)
@ConstructedBy(AppDatabaseConstructor::class)
@TypeConverters(Converters::class) // remove if you don't need.
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
}


fun getRoomDatabaseBuilder(
    builder: RoomDatabase.Builder<AppDatabase>
): AppDatabase {
    return builder
        .fallbackToDestructiveMigrationOnDowngrade(true)
        .setDriver(BundledSQLiteDriver())
        .build()
}

@Suppress("NO_ACTUAL_FOR_EXPECT")
expect object AppDatabaseConstructor :RoomDatabaseConstructor<AppDatabase> {
    override fun initialize(): AppDatabase
}

As we spoke, to store complex data you will need a converter class, and you will need Kotlin serialization dependency too.


create new class "Converters.kt" //optional 🔨

import androidx.room.ProvidedTypeConverter
import androidx.room.TypeConverter
import kotlinx.serialization.encodeToString
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json

@ProvidedTypeConverter
class Converters {

    @TypeConverter
    fun fromStringList(value: List<String>): String {
        return Json.encodeToString(value)
    }

    @TypeConverter
    fun toStringList(value: String): List<String> {
        return Json.decodeFromString(value)
    }
}

Now, Final set up for our platform specific Database Builders

First open AndroidMain module, use your same project structure, create db package and create kotlin file "AppDatabase.android.kt" and add following code. 🤖


import android.content.Context
import androidx.room.Room
import androidx.room.RoomDatabase

fun getDatabaseBuilder(
    context: Context
):  RoomDatabase.Builder<AppDatabase>  {
    val appContext = context.applicationContext

    val dbFile = appContext.getDatabasePath("user_database.db")
    return Room.databaseBuilder<AppDatabase>(
        context = appContext,
        name = dbFile.absolutePath
    )
}

Now, for iOSMain, use same pattern and create file "AppDatabase.ios.kt" in iosMain Module.


import androidx.room.Room
import androidx.room.RoomDatabase
import kotlinx.cinterop.ExperimentalForeignApi
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSFileManager
import platform.Foundation.NSUserDomainMask

fun getDatabaseBuilder() :  RoomDatabase.Builder<AppDatabase>  {
    val dbFile = documentDirectory() + "/user_database.db"
    return Room.databaseBuilder<AppDatabase>(
        name = dbFile,
    )

}

@OptIn(ExperimentalForeignApi::class)
private fun documentDirectory(): String {
    val documentDirectory = NSFileManager.defaultManager.URLForDirectory(
        directory = NSDocumentDirectory,
        inDomain = NSUserDomainMask,
        appropriateForURL = null,
        create = false,
        error = null,
    )
    return requireNotNull(documentDirectory?.path)
}

For JVM(Desktop), create file "AppDatabase.jvm.kt" in desktop module.


// 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,
    )
}

Finally, pass this builder object to your main shared App() composable. Let's see How?


Go to Shared or common module and open your App.kt file. and add following parameters. And see how to get Dao and database object.


Pro Tip 💡 : You should use view model instead of accessing dao object direclty on UI.


@Composable
@Preview
fun App(
    databaseBuilder: RoomDatabase.Builder<AppDatabase>
) {
	val userDatabase = getRoomDatabase(databaseBuilder)
	  val userDao = userDatabase.userDao()

		// your rest of code
}

Finally, don't forget to pass value of databaseBuilder parameter from each platform. for android module, go to MainActivity.kt and pass parameter.

class MainActivity : ComponentActivity() {
   
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            App(
                databaseBuilder = getDatabaseBuilder(this)
            )
        }
    }
}

Go to MainViewController for iosMain and pass argument

fun MainViewController() = ComposeUIViewController {
  val databaseBuilder = remember { getDatabaseBuilder() }
    App(
        databaseBuilder = databaseBuilder
    )
}


Thank you for reading this artcial, Hope you learn something new today. If you find any errors/mistakes please feel free to reach me via chat button on my website. Happy coding ! 💻✨

 
 
 

Comments

Couldn’t Load Comments
It looks like there was a technical problem. Try reconnecting or refreshing the page.

Available for Select
freelance opportunities

Have an exciting project you need 

help with?

Send me an email or contact me via

instant message!

© 2022 by Nimesh Vasani. Mobile Developer

icons8-quotation-48.png
Screenshot 2024-03-20 at 5.53.00 PM.png

You can't connect the dots looking forward; you can only connect them looking backwards. So you have to trust that the dots will somehow connect in your future. You have to trust in something - your gut, destiny, life, karma, whatever. This approach has never let me down, and it has made all the difference in my life.

I’m as proud of many of the things we haven’t done as the things we have done. Innovation is saying no to a thousand things.

- Steve Jobs

  Founder of Apple Inc.

icons8-quotation-48.png
Screenshot 2024-03-20 at 6.01.37 PM.png

Clean code always looks like it was written by someone who cares. Most good programmers do programming not because they expect to get paid or get adulation by the public, but because it is fun to program

- Robert C Martin 

    Known For Agile Manifesto.

icons8-quotation-48.png
Screenshot 2024-03-20 at 6.01.59 PM.png

When I wrote this code, only God and I understood what I did. Now only God knows. 

 – Anonymous

bottom of page