0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RoomはAndroidアプリ開発で利用するJetpackライブラリの一つです。
SQLiteのデータベースの構築や、データの挿入、取得などを実装する際に利用します。

Roomのバージョンが2.7.0になり、Kotlin Multiplatform(KMP)で利用可能になりました。
この記事ではKMPかつCompose MultiplatformでのRoomの利用方法を記載しています。(一般的なRoomの利用方法は記載していません)

導入

KMPのプロジェクトにRoomを導入します。

libs.version.toml

各種ライブラリを追記します。

libs.versions.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を修正し、各種ライブラリの導入とセットアップを行います。

build.gradle.kts(:composeApp)
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")
}

dependenciesroom-compilerを導入する方法が、Androidのみのプロジェクトの場合と異なります。
Androidのみのプロジェクトの場合、

build.gradle.kts
dependencies {
    ksp(libs.androidx.room.compiler)
}

このように導入しますが、KMPのプロジェクトの場合にこの方法で導入すると、iOSでビルド時にエラーとなります。

Database、Entity、Daoの実装

AndroidとiOSでRoomを用いた実装を共通化するため、共通モジュールに実装します。
特筆する部分がないためサンプルコードだけ記載しておきます。

Database

AppDatabas.kt
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

PersonEntity.kt
import androidx.room.Entity
import androidx.room.PrimaryKey

@Entity
data class PersonEntity(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    val name: String
)

Dao

PersonDao.kt
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>を追加します。

Platform.kt
import androidx.room.RoomDatabase

// ...

expect object AppContext

expect fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase>

AndroidではDatabase Builder作成時に、Contextが必要となるためexpect object AppContextも追加しています。

Androidモジュール

Androidモジュールにactual object AppContextactual fun getDatabaseBuilder(): RoomDatabase.Builder<AppDatabase>を実装します。

Platform.android.kt
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を以下のように修正します。

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>を実装します。

Platform.ios.kt
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

AppViewModel.kt
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

App.kt
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}")
                    }
                }
            }
        }
    }
}
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?