RoomはAndroidアプリ開発で利用するJetpackライブラリの一つです。
SQLiteのデータベースの構築や、データの挿入、取得などを実装する際に利用します。
Roomのバージョンが2.7.0になり、Kotlin Multiplatform(KMP)で利用可能になりました。
この記事ではKMPかつCompose MultiplatformでのRoomの利用方法を記載しています。(一般的なRoomの利用方法は記載していません)
導入
KMPのプロジェクトにRoomを導入します。
libs.version.toml
各種ライブラリを追記します。
[versions]
# ...
kotlin = "2.1.20"
ksp = "2.1.20-2.0.0"
androidx-room = "2.7.0"
sqlite-bundled = "2.5.0"
[libraries]
# ...
androidx-room-compiler = { group = "androidx.room", name = "room-compiler", version.ref = "androidx-room" }
androidx-room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "androidx-room" }
androidx-sqlite-bundled = { group = "androidx.sqlite", name = "sqlite-bundled", version.ref = "sqlite-bundled" }
[plugins]
# ...
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
room = { id = "androidx.room", version.ref = "androidx-room" }
- KMPに対応しているRoomのバージョンは2.7.0以上(2.7.0は2025年4月9日にリリースされています)
- KotlinとKspのバージョンを統一(統一しないとビルド時にエラーとなります)
Gradleファイル
KMPの共通モジュールのbuild.gradle.kts
を修正し、各種ライブラリの導入とセットアップを行います。
plugins {
// ...
alias(libs.plugins.ksp)
alias(libs.plugins.room)
}
kotlin {
// ...
sourceSets {
// ...
commonMain.dependencies {
// ...
implementation(libs.androidx.room.runtime)
implementation(libs.androidx.sqlite.bundled)
}
}
}
// ...
dependencies {
// ...
add("kspCommonMainMetadata", libs.androidx.room.compiler)
add("kspAndroid", libs.androidx.room.compiler)
add("kspIosSimulatorArm64", libs.androidx.room.compiler)
add("kspIosX64", libs.androidx.room.compiler)
add("kspIosArm64", libs.androidx.room.compiler)
}
room {
schemaDirectory ("$projectDir/schemas")
}
dependencies
でroom-compiler
を導入する方法が、Androidのみのプロジェクトの場合と異なります。
Androidのみのプロジェクトの場合、
dependencies {
ksp(libs.androidx.room.compiler)
}
このように導入しますが、KMPのプロジェクトの場合にこの方法で導入すると、iOSでビルド時にエラーとなります。
Database、Entity、Daoの実装
AndroidとiOSでRoomを用いた実装を共通化するため、共通モジュールに実装します。
特筆する部分がないためサンプルコードだけ記載しておきます。
Database
import androidx.room.ConstructedBy
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.RoomDatabaseConstructor
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
@Database(entities = [PersonEntity::class], version = 1)
@ConstructedBy(AppDatabaseConstructor::class)
abstract class AppDatabase : RoomDatabase() {
abstract fun getPersonDao(): PersonDao
}
@Suppress("NO_ACTUAL_FOR_EXPECT")
expect object AppDatabaseConstructor : RoomDatabaseConstructor<AppDatabase> {
override fun initialize(): AppDatabase
}
fun getRoomDatabase(builder: RoomDatabase.Builder<AppDatabase>): AppDatabase {
return builder
.addMigrations()
.fallbackToDestructiveMigrationOnDowngrade(true)
.setDriver(BundledSQLiteDriver())
.setQueryCoroutineContext(Dispatchers.IO)
.build()
}
Entity
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity
data class PersonEntity(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val name: String
)
Dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
@Dao
interface PersonDao {
@Insert
suspend fun insert(person: PersonEntity)
@Query("SELECT * FROM PersonEntity")
suspend fun getAll(): List<PersonEntity>
}
Database Builderの実装
Android、iOSでRoomを利用するために、それぞれのOS固有のシステムを利用したDatabase Builderを実装する必要があります。
共通モジュール
Compose MultiPlatformではUIもAndroidとiOSで共通となります。
よって共通モジュールは、AndroidモジュールもしくはiOSモジュールで作成したDatabase Builderインスタンスを取得する必要があるため、expect
actual
パターンを利用して実装します。
まずは共通モジュールにexpect fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase>
を追加します。
import androidx.room.RoomDatabase
// ...
expect object AppContext
expect fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase>
AndroidではDatabase Builder作成時に、Context
が必要となるためexpect object AppContext
も追加しています。
Androidモジュール
Androidモジュールにactual object AppContext
とactual fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase>
を実装します。
import android.content.Context
import androidx.room.Room
import androidx.room.RoomDatabase
import java.lang.ref.WeakReference
// ...
actual object AppContext {
private var value: WeakReference<Context?>? = null
fun set(context: Context) {
value = WeakReference(context)
}
internal fun get(): Context {
return value?.get() ?: throw RuntimeException("Context Error")
}
}
actual fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
val appContext = AppContext.get().applicationContext
val dbFile = appContext.getDatabasePath("app_room.db")
return Room.databaseBuilder<AppDatabase>(
context = appContext,
name = dbFile.absolutePath
)
}
Androidアプリ実行時に、最初にContext
をsetしておく必要があるため、MainActivity.kt
を以下のように修正します。
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 以下を記載
AppContext.apply { set(applicationContext) }
setContent {
App()
}
}
}
iOSモジュール
iOSモジュールにactual fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase>
を実装します。
import androidx.room.Room
import androidx.room.RoomDatabase
import kotlinx.cinterop.ExperimentalForeignApi
import platform.Foundation.NSDocumentDirectory
import platform.Foundation.NSFileManager
import platform.Foundation.NSUserDomainMask
// ...
actual object AppContext
actual fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase> {
val dbFilePath = documentDirectory() + "/app_room.db"
return Room.databaseBuilder<AppDatabase>(
name = dbFilePath,
)
}
@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)
}
iOSではDatabase Builder作成時に、Context
が必要ではないため、actual object AppContext
は形だけ実装します。
UIとViewModelの実装
あとはViewModel
でデータを取得する処理とデータを挿入する処理を作成しUIに反映するだけとなります。サンプルコードは以下となります。
ViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
class AppViewModel : ViewModel() {
private val personDao = getRoomDatabase(getDatabaseBuilder()).getPersonDao()
private val _personList = MutableStateFlow(emptyList<PersonEntity>())
val personList: StateFlow<List<PersonEntity>> = _personList.asStateFlow()
init {
viewModelScope.launch {
_personList.value = personDao.getAll()
}
}
fun insertPerson(name: String) {
val person = PersonEntity(name = name)
viewModelScope.launch {
personDao.insert(person)
val currentPersonList = _personList.value.toMutableList()
currentPersonList.add(person)
_personList.value = currentPersonList
}
}
}
Composable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material.TextField
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import org.jetbrains.compose.ui.tooling.preview.Preview
@Composable
@Preview
fun App() {
MaterialTheme {
val viewModel = AppViewModel()
val personList by viewModel.personList.collectAsState()
var name by remember { mutableStateOf("") }
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
TextField(
value = name,
onValueChange = { name = it },
label = { Text("name") }
)
Button(
onClick = { viewModel.insertPerson(name = name) },
content = { Text("Click!") },
)
}
LazyColumn(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp),
horizontalAlignment = Alignment.Start,
) {
items(items = personList) { person ->
Row {
Text("id : ${person.id} / ")
Text("name : ${person.name}")
}
}
}
}
}
}