Hi all! Anna Zharkova, head of the mobile development group at Usetech, is in touch. At the beginning of May, Google pleased us with the release of several libraries for local storage. Finally, Kotlin Multiplatform applications can be fully used Room (version 2.7.0-alpha01 and higher).
And today we will try working with this library using the example of a small Todo applicationwritten in KMP using Compose Multiplatform.
Let’s start with the project settings. We will need to install the Room library and SQLite (its dependency). Let’s add the dependency to the lib.versions directory:
/*lib.versions*/
[versions]
\\..
androidxRoom = "2.7.0-alpha01"
sqlite = "2.5.0-alpha01"
[libraries]
\\..
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidxRoom" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidxRoom" }
sqlite-bundled = { module = "androidx.sqlite:sqlite-bundled", version.ref = "sqlite" }
sqlite = { module = "androidx.sqlite:sqlite", version.ref = "sqlite" }
Please note that we specify the compiler and runtime for Room. SQLite is the default storage we use under the hood of Room.
We also need to connect the plugin for Room:
/*lib.versions*/
[plugins]
\\...
room = { id = "androidx.room", version.ref = "androidxRoom" }
/*build.gradle.kts app*/
plugins {
\\...
alias(libs.plugins.room).apply(false)
}
/*build.gradle.kts shared*/
plugins {
\\...
alias(libs.plugins.room)
}
Don’t forget to add the commonMain target to the dependency block:
sourceSets {
commonMain.dependencies {
implementation(libs.androidx.room.runtime)
implementation(libs.sqlite.bundled)
implementation(libs.sqlite)
}
}
We start synchronization and get an error. Because they didn’t add KSP. One of the main stages of Room’s migration was the transition from KAPT to KSP, which made multi-platform support possible. Therefore, for correct operation we need to install the KSP plugin:
/*lib.versions*/
[versions]
\\...
ksp = "1.9.23-1.0.19"
kotlin = "1.9.23"
\\...
[plugins]
\\...
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
Please note that Kotlin version must match the KSP major version.
/*build.gradle.kts app*/
plugins {
\\...
alias(libs.plugins.ksp) apply false
}
/*build.gradle.kts shared*/
plugins {
\\...
id("com.google.devtools.ksp")
}
We will also add to the very bottom of build.gradle.kts (shared) a block for processing Room modules via KSP:
dependencies {
add("kspAndroid", libs.androidx.room.compiler)
add("kspIosSimulatorArm64", libs.androidx.room.compiler)
add("kspIosX64", libs.androidx.room.compiler)
add("kspIosArm64", libs.androidx.room.compiler)
}
We also indicate the path to search for database schemas:
room {
schemaDirectory("$projectDir/schemas")
}
Synchronizing Gradle.
Done, we have installed Room. Now let’s set up our storage.
Just like in the Android application, we will need to take the following steps (with some nuances):
1. Create an Entity data model for the database table.
2. Create a Dao for queries from our table.
3. Set up the storage as a successor to RoomDatabase.
4. Create a repository for requests – an optional step, more to maintain architectural order.
So, for the data model we use a regular data class with the fields we need. Let’s add the @`Entity annotation to generate a table from the model. The @`PrimaryKey annotation will mark the primary key field:
@Entity
data class TodoEntity(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val content: String
val date: String
)
Now let’s add a Dao interface with methods for adding an element (Insert) and getting data (Select):
@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>>
}
Let’s move on to the most interesting part – creating a database. As usual, we create an abstract descendant class RoomDatabase:
@Database(entities = [TodoEntity::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun getDao(): TodoDao
}
Let’s add a builder to it taking into account expect/actual:
//Android
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
fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
val dbFilePath = NSHomeDirectory() + "/my_room.db"
return Room.databaseBuilder<AppDatabase>(
name = dbFilePath,
factory = { AppDatabase::class.instantiateImpl() }
)
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
}
Our builders have different signatures, so we cannot mark them as actual and set a common signature with expect. Let’s try to solve the problem as follows: we will use Koin to initialize the storage and create an expect/actual module.
//commonMain
expect fun platformModule(): Module
//androidMain
actual fun platformModule() = module {
single<AppDatabase> { getDatabase(get()) }
}
//iOSMain
actual fun platformModule() = module {
single<AppDatabase> { getDatabase() }
}
Now a little challenge: pass the context from the Android application side? Let’s create a function in commonMain with a block parameter:
fun initKoin(appDeclaration: KoinAppDeclaration = {}) =
startKoin {
appDeclaration()
modules(platformModule())
}
Let’s also add a singleton factory to access di:
object Koin {
var di: KoinApplication? = null
fun setupKoin(appDeclaration: KoinAppDeclaration = {}) {
if (di == null) {
di = initKoin(appDeclaration)
}
}
}
We will call Koin.setupKoin() from native Android and iOS applications:
Koin.setupKoin {
androidContext(applicationContext)
}
Finally, we are done with initializations and settings. Let’s move on to connecting the logic for working with storage to application screens.
Let’s add a repository. where we call the Dao methods:
class TaskRepository(private val database: AppDatabase) {
private val dao: TodoDao by lazy {
database.getDao()
}
suspend fun addTodo(todoEntity: TodoEntity) {
dao.insert(todoEntity)
}
suspend fun loadTodos(): Flow<List<TodoEntity>> {
return dao.getAllAsFlow()
}
}
And let’s add calling functions to our ViewModel. To add an entry:
class AddTodoViewModel(
private val taskRepository: TaskRepository
) : ViewModel() {
val titleText: MutableStateFlow<String> = MutableStateFlow<String>("")
fun onConfirm() {
viewModelScope.launch {
taskRepository.addTodo(TodoEntity(title = titleText.value))
}
}
}
And actually, the call to load:
class TodoViewModel(private val repository: TaskRepository) : ViewModel() {
val tasks: MutableSharedFlow<List<TodoEntity>> = MutableSharedFlow(1, onBufferOverflow = BufferOverflow.DROP_OLDEST)
fun loadData() {
viewModelScope.launch {
repository.loadTodos().collectLatest {
tasks.tryEmit(it)
}
}
}
}
Checking the work:
Let’s try to run it on iOS. Objectively, generating such a simple circuit took several minutes.
You may also encounter compilation and ksp generation errors. The API is experimental and not without bugs.
Try specifying toolChain and Kotlin version for the compiler:
kotlin {
jvmToolchain(17)
}
//...
compilerOptions {
languageVersion.set(KOTLIN_1_9)
}
Let’s check the result:
Our finished project:
github.com/anioutkazharkova/room-kmp
Room KMP Limitations
There are also differences in the versions of Room for Kotlin Multiplatform. For example, using methods marked with the @`RawQuery annotation in non-Android targets will cause an error. Support for this annotation will be added in future versions of Room.
Also supported only on Android:
1 Callback API:
- RoomDatabase.Builder.setQueryCallback,
- RoomDatabase.QueryCallback
2 Automatic closing of the database due to timeout:
- RoomDatabase.Builder.setAutoCloseTimeout
3 Multiple storage instances:
- RoomDatabase.Builder.enableMultiInstanceInvalidation
4 Creating a database from assets, files, etc.:
- RoomDatabase.Builder.createFromAsset,
- RoomDatabase.Builder.createFromFile,
- RoomDatabase.Builder.createFromInputStream,
- RoomDatabase.PrepackagedDatabaseCallback
Promised in future versions – we’re waiting.
Thanks for your attention, stay in touch.
developer.android.com/kotlin/multiplatform/sqlite
developer.android.com/kotlin/multiplatform/room
johnoreilly.dev/posts/jetpack_room_kmp
Acknowledgement and Usage Notice
The editorial team at TechBurst Magazine acknowledges the invaluable contribution of the author of the original article that forms the foundation of our publication. We sincerely appreciate the author’s work. All images in this publication are sourced directly from the original article, where a reference to the author’s profile is provided as well. This publication respects the author’s rights and enhances the visibility of their original work. If there are any concerns or the author wishes to discuss this matter further, we welcome an open dialogue to address potential issues and find an amicable resolution. Feel free to contact us through the ‘Contact Us’ section; the link is available in the website footer.