TK-BLOG

Roomのセットアップ

依存関係の追加

build.gradle.kts
import org.jetbrains.kotlin.gradle.dsl.JvmTarget

plugins {
    // 以下を追加
    alias(libs.plugins.ksp)
    alias(libs.plugins.room)
}

kotlin {
    sourceSets {
        androidMain.dependencies {
		    // 以下を追加
            implementation(libs.androidx.room.runtime.android)
            implementation(libs.koin.android)
        }
        commonMain {
            // 以下を追加
            kotlin.srcDir("build/generated/ksp/metadata")
            dependencies {
                // 以下を追加
                implementation(libs.androidx.room.runtime)
                implementation(libs.sqlite.bundled)
                api(libs.koin.core)
                implementation(libs.koin.compose)
                implementation(libs.koin.compose.viewmodel)
            }
        }
    }
}

dependencies {
    // 以下を追加
    // Android
    add("kspAndroid", libs.androidx.room.compiler)
    // iOS
    add("kspIosSimulatorArm64", libs.androidx.room.compiler)
    add("kspIosX64", libs.androidx.room.compiler)
    add("kspIosArm64", libs.androidx.room.compiler)
    debugImplementation(compose.uiTooling)
}

// 以下を追加
room {
    schemaDirectory("$projectDir/schemas")
}

テーブル・DAOの作成

TaskEntity.kt
package com.app.app.data.local.entity

import androidx.room.Entity
import androidx.room.PrimaryKey

/**
 * タスクテーブルを表すEntityクラス
 */
@Entity(tableName = "task")
data class TaskEntity(
    @PrimaryKey(autoGenerate = true) val id: Long?,
    val title: String,
)
TaskDao.kt
package com.app.app.data.local.dao

import androidx.room.Dao
import androidx.room.Insert
import androidx.room.Query
import com.app.app.data.local.entity.TaskEntity
import kotlinx.coroutines.flow.Flow

/**
 * DBへのアクセスを提供するインターフェース
 * Roomが実装し、ローカルのSQLiteとやり取りを行う
 */
@Dao
interface TaskDao {
    @Insert
    suspend fun insert(task: TaskEntity)

    @Query("SELECT count(*) FROM task")
    suspend fun count(): Int

    @Query("SELECT * FROM task")
    fun selectAll(): Flow<List<TaskEntity>>
}

DBの作成・取得

AppDatabase.kt
package com.app.app.data.local

import androidx.room.*
import com.app.app.data.local.dao.TaskDao
import com.app.app.data.local.entity.TaskEntity

/**
 * DBのファイル名を定義
 */
internal const val DB_FILE_NAME = "app.db"

/**
 * データベースを表すクラス
 */
@Database(entities = [TaskEntity::class], version = 1)
@ConstructedBy(AppDatabasebaseConstructor::class)
abstract class AppDatabase : RoomDatabase() {
    abstract fun getTaskDao(): TaskDao
}

/**
 * データベースのインスタンスを取得する
 */
@Suppress("EXPECT_ACTUAL_CLASSIFIERS_ARE_IN_BETA_WARNING")
expect object AppDatabasebaseConstructor : RoomDatabaseConstructor<AppDatabase> {
    override fun initialize(): AppDatabase
}

DIモジュールの定義

Koin.kt
package com.app.app.di

import org.koin.core.context.startKoin
import org.koin.core.module.Module
import org.koin.dsl.KoinAppDeclaration

/**
 * 各プラットフォームで異なるモジュールを提供するための関数
 */
expect fun platformModule(): Module

/**
 * DIフレームワークの初期化を行う
 */
fun initKoin(appDeclaration: KoinAppDeclaration = {}) =
    startKoin {
        appDeclaration()
        modules(
            platformModule(),
        )
    }

各プラットフォームごとのDB作成・取得

Android

AppDatabase.android.kt
package com.app.app.data.local

import android.content.Context
import androidx.room.Room
import androidx.sqlite.driver.bundled.BundledSQLiteDriver

/**
 * Android向けのデータベースインスタンスを取得する関数
 */
fun getDatabase(context: Context): AppDatabase {
    val dbFile = context.getDatabasePath(DB_FILE_NAME)
    return Room.databaseBuilder<AppDatabase>(
        context = context.applicationContext,
        name = dbFile.absolutePath,
    )
        .setDriver(BundledSQLiteDriver())
        .fallbackToDestructiveMigration(true)
        .build()
}

iOS

AppDatabase.ios.kt
package com.app.app.data.local

import androidx.room.Room
import androidx.sqlite.driver.bundled.BundledSQLiteDriver
import kotlinx.cinterop.ExperimentalForeignApi
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.IO
import platform.Foundation.*

/**
 * iOS向けのデータベースインスタンスを取得する関数
 */
@OptIn(ExperimentalForeignApi::class)
fun getDatabase(): AppDatabase {
    val documentDirectory =
        NSFileManager.defaultManager.URLForDirectory(
            directory = NSDocumentDirectory,
            inDomain = NSUserDomainMask,
            appropriateForURL = null,
            create = false,
            error = null,
        )
    val dbFilePath = "${requireNotNull(documentDirectory?.path)}/$DB_FILE_NAME"
    return Room.databaseBuilder<AppDatabase>(name = dbFilePath)
        .setDriver(BundledSQLiteDriver())
        .setQueryCoroutineContext(Dispatchers.IO)
        .build()
}

各プラットフォームごとのDI

Android

Koin.android.kt
package com.app.app.di

import com.app.app.data.local.AppDatabase
import com.app.app.data.local.getDatabase
import org.koin.core.module.Module
import org.koin.dsl.module

/**
 * Android用のプラットフォームモジュール
 */
actual fun platformModule(): Module =
    module {
        single<AppDatabase> { getDatabase((get())) }
    }
MainActivity.kt
package com.app.app

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import com.app.app.di.initKoin
import org.koin.android.ext.koin.androidContext

/**
 * Androidアプリのエントリーポイント関数
 */
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Koinの初期化
        initKoin(appDeclaration = { androidContext(this@MainActivity) })

        setContent {
            App()
        }
    }
}

iOS

Koin.ios.kt
package com.app.app.di

import com.app.app.data.local.AppDatabase
import com.app.app.data.local.getDatabase
import org.koin.core.module.Module
import org.koin.dsl.module

/**
 * iOS用のプラットフォームモジュール
 */
actual fun platformModule(): Module =
    module {
        singleDatabase> { getDatabase() }
    }
MainViewController.kt
package com.app.app

import androidx.compose.ui.window.ComposeUIViewController
import com.app.app.di.initKoin
import platform.UIKit.UIViewController

/**
 * iOSアプリのエントリーポイント関数
 */
fun mainViewController(): UIViewController {
    // Koinの初期化
    initKoin()
    return ComposeUIViewController { App() }
}