Roomは処理の開始時にコールバックを受け取ることができます。
このコールバックついて、まとめました。
※環境:Android Studio Ladybug | 2024.2.1 Patch 3
    Kotlin 2.0.0
    androidx.sqlite:sqlite:2.4.0
    androidx.room:room-*:2.6.1
    DB Browser for SQLite バージョン 3.13.1
※サンプルの全体像は「Roomでデータベースを構築」を参照
目次
コールバックの受け取り
Roomは開始時に、2つのコールバックRoomDatabase.Callback#onCreateと#onOpenを受け取ることが出来ます。
| タイミング | スレッド | ||
|---|---|---|---|
| onCreate | アプリの起動後に初めて データベースアクセスが 行われた時 | アプリの起動後に初めて データベースファイルが 作成された時 | データの登録・検索を 行ったスレッド | 
| onOpen | アプリの起動後に初めて データベースファイルが 開かれた時 | ||
| ※RoomDatabaseを取り出したタイミングと異なる | |||
@Database(entities = arrayOf(Player::class), version = 1)
abstract class GameDb : RoomDatabase() {
    abstract fun playerDao(): PlayerDao     // Daoの提供(必須)
    companion object {
        @Volatile
        private var instance: GameDb? = null
        fun getDatabase(context: Context): GameDb {
            return instance ?: synchronized(this) {
                val _instans = Room.databaseBuilder(
                    context,
                    GameDb::class.java,
                    "Game.db"     // データベース名(ファイル名)
                )
                    .addCallback(object : Callback() {
                        // データベースファイルが作成された時
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            super.onCreate(db)
                            Log.i(TAG, "DB onCreate ! [${getThreadName()}]")
                        }
                        // データベースファイルが開かれた時
                        override fun onOpen(db: SupportSQLiteDatabase) {
                            super.onOpen(db)
                            Log.i(TAG, "DB onOpen ! [${getThreadName()}]")
                        }
                    })
                    .build()
                instance = _instans
                _instans
            }
        }
    }
}
コールバックのタイミング
コールバックが呼び出されるタイミングは、データベースをオープンした(識別子RoomDatabaseを取り出した)時ではありません。
アプリの起動後に初めてデータベースアクセス(登録・検索など)が行われた時です。
この時、
データベースファイルが存在しなければ、作成し、onCreateを呼び出します。存在すれば、onCreateは呼ばれません。
その後、データベースファイルを開き、onOpenを呼び出します。
class RankingActivity : ComponentActivity() {
    private lateinit var gameDb: GameDb
    private lateinit var playerFlow: MutableStateFlow<List<Player>>
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent { ... }
        gameDb = (application as MyApplication).gameDb
        playerFlow = MutableStateFlow<List<Player>>(listOf<Player>())
        lifecycleScope.launch(Dispatchers.Default) {
            val _list = gameDb.playerDao().fetchTopX(10) // 初めてアクセス
            playerFlow.value = _list
        }
    }
}
RoomはSQLite APIのラッパーAPIであって、内部でSQLite APIを呼び出しています。
つまり、マネージメントシステムとやり取り(問い合わせ⇔返答)を行っているのは、SQLite APIです。

コールバックはSQLite APIの動作に連動しています。
SQLite APIはデータベースをオープンした(識別子SQLiteDatabaseを取り出した)時に、データベースファイルを作成したり、開いたりします。ですので、このタイミングでコールバックは呼び出されます。
コールバックのスレッド
コールバックを実行するスレッドは、引き金になったデータベースアクセスを実行したスレッドです。
class RankingActivity : ComponentActivity() {
    private lateinit var gameDb: GameDb
    private lateinit var playerFlow: MutableStateFlow<List<Player>>
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent { ... }
        gameDb = (application as MyApplication).gameDb
        playerFlow = MutableStateFlow<List<Player>>(listOf<Player>())
        lifecycleScope.launch(Dispatchers.Default) { Thread.sleep(100) }  // ...-worker-1
        lifecycleScope.launch(Dispatchers.Default) { Thread.sleep(100) }  // ...-worker-2
        lifecycleScope.launch(Dispatchers.Default) {					  // ...-worker-3
            Log.i(TAG, "First fetch ! [${getThreadName()}]")
            val _list = gameDb.playerDao().fetchTopX(10) // 初めてアクセス
            playerFlow.value = _list
        }
    }
}
First fetch ! [DefaultDispatcher-worker-3] DB onCreate ! [DefaultDispatcher-worker-3] DB onOPen ! [DefaultDispatcher-worker-3]
データの事前取り込み
コールバックを用いて、データの事前取り込み(初期状態の設定)が出来ます。
しかし、制約があるのでお勧めしません。ファイルを用いる方法が最良であると思います。※詳細は「Room Database:データの事前取り込み」を参照
(1)引数db:SupportSQLiteDatabaseを利用
コールバックonCreateは引数にSupportSQLiteDatabaseを持ちます。
この識別子を用いれば、SQLite APIと同じ方法でSQLを発行し、データの事前取り込みを行うことができます。
※SupportSQLiteDatabase:SQLite APIのデータベース識別子(SQLiteDatabase)へ橋渡しする関数、SQLiteDatabaseとほぼ等価
※SQLite APIデータベースの事前取り込みについては「SQLite API Database:データの事前取り込み」を参照
@Database(entities = arrayOf(Player::class), version = 1)
abstract class GameDb : RoomDatabase() {
    abstract fun playerDao(): PlayerDao     // Daoの提供(必須)
    companion object {
        @Volatile
        private var instance: GameDb? = null
        fun getDatabase(context: Context): GameDb {
            return instance ?: synchronized(this) {
                val _instans = Room.databaseBuilder(
                    context,
                    GameDb::class.java,
                    "Game.db"     // データベース名(ファイル名)
                )
                    .addCallback(object : Callback() {
                        // データベースが作成された時
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            super.onCreate(db)
                            repeat(10) {
                                val _player = Player(0, "ZZZZZZZZ", 0)
                                val _values = ContentValues().apply {
                                    put("name", _player.name)
                                    put("score", _player.score)
                                }
                                db.insert("Player", CONFLICT_NONE, _values)
                            }
                        }
                    })
                    .build()
                instance = _instans
                _instans
            }
        }
    }
}
ただし、Columnのフィールド名、テーブル名をソースコード中に埋め込む必要があります。
アノテーション@Daoからシンボルプロセッサにより、Dao関数群を自動生成する利点が半減してしまいます。
(2)Daoの利用(エラーで落ちる)
(1)を改善し、Dao関数を用いてデータの事前取り込みを行います。
@Database(entities = arrayOf(Player::class), version = 1)
abstract class GameDb : RoomDatabase() {
    abstract fun playerDao(): PlayerDao     // Daoの提供(必須)
    companion object {
        @Volatile
        private var instance: GameDb? = null
        fun getDatabase(context: Context): GameDb {
            return instance ?: synchronized(this) {
                val _instans = Room.databaseBuilder(
                    context,
                    GameDb::class.java,
                    "Game.db"     // データベース名(ファイル名)
                )
                    .addCallback(object : Callback() {
                        // データベースが作成された時
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            super.onCreate(db)
                            instance?.let {
                                val _dao = it.playerDao()
                                repeat(10) {  // 事前データの登録
                                    _dao.insert(Player(-1, "ZZZZZZZZ", 0))
                                }
                            }
                        }
                    })
                    .build()
                instance = _instans
                _instans
            }
        }
    }
}
ただし、この方法は例外を発生して、アプリが落ちてしまいます。
原因は、スレッド内で処理がループする(赤字部分)からです。
(1)データベースアクセス(アプリの初回データ取得)
(2-1)識別子の取り出し(SQlite API)           ⇐ 1回目のデータベースオープン
(2-2)データベースが無い⇒ファイル作成
(3)onCreateを実行
(4)データベースアクセス(事前データの登録)
(5-1)識別子の取り出し(SQlite API)           ⇐ 2回目のデータベースオープン
(5-2)データベースが無い⇒ファイル作成
(6)例外発生 ⇒ データベースクローズ
FATAL EXCEPTION: DefaultDispatcher-worker-1
Process: パッケージ名, PID: 8287
java.lang.IllegalStateException: Closed during initialization
    at android.database.sqlite.SQLiteOpenHelper.close(SQLiteOpenHelper.java:452)
    at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.innerGetDatabase(FrameworkSQLiteOpenHelper.kt:180)
    at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper$OpenHelper.getSupportDatabase(FrameworkSQLiteOpenHelper.kt:151)
    at androidx.sqlite.db.framework.FrameworkSQLiteOpenHelper.getWritableDatabase(FrameworkSQLiteOpenHelper.kt:104)
    at androidx.room.RoomDatabase.inTransaction(RoomDatabase.kt:632)
    at androidx.room.RoomDatabase.assertNotSuspendingTransaction(RoomDatabase.kt:451)
    at パッケージ名.PlayerDao_GameDb_Impl.insert(PlayerDao_GameDb_Impl.java:65)
    at パッケージ名.db.GameDb$Companion$getDatabase$1$_instans$1.onCreate(GameDb.kt:43)
    ...
(3)Daoの利用+スレッド(レーシングを発生)
(2)を改善し、「事前データの取り込み」と「アプリの初回データ取得」を別スレッドに分離します。
異なるスレッドから同時刻にSQLが送られて来ても、マネージメントシステムが正しく処理できるように調整してくれます。
@Database(entities = arrayOf(Player::class), version = 1)
abstract class GameDb : RoomDatabase() {
    abstract fun playerDao(): PlayerDao     // Daoの提供(必須)
    companion object {
        @Volatile
        private var instance: GameDb? = null
        fun getDatabase(context: Context, scope: CoroutineScope): GameDb {
            return instance ?: synchronized(this) {
                val _instans = Room.databaseBuilder(
                    context,
                    GameDb::class.java,
                    "Game.db"     // データベース名(ファイル名)
                )
                    .addCallback(object : Callback() {
                        // データベースが作成された時
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            super.onCreate(db)
                            instance?.let {
                                scope.launch(Dispatchers.Default) {
                                    val _dao = it.playerDao()
                                    repeat(10) {
                                        _dao.insert(Player(0, "ZZZZZZZZ", 0))
                                    }
                                }
                            }
                        }
                    })
                    .build()
                instance = _instans
                _instans
            }
        }
    }
}
ただし、このスレッド間の調整はSQL単位なので、「事前データの取り込み」と「アプリの初回データ取得」がレーシング(競合)します。
関連記事:
