You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何确保RoomDatabase.Callback中的协程预填充操作完成后再执行后续逻辑

解决方案

核心问题原因

你当前的时序问题根因在于两个异步逻辑没有依赖关系:

  • Room.databaseBuilder.build() 方法只会完成数据库实例的初始化,不会等待 RoomDatabase.Callback.onCreate 中启动的协程执行完成
  • prePopulateDatabase 是在独立的协程中异步执行,和你Fragment中启动的查询逻辑没有先后约束,自然会出现查询早于预填充完成的情况

方案一:通过 CompletableDeferred 监听预填充完成状态(推荐)

这是适配协程场景最轻量的方案,不需要改变原有代码的结构,只需要新增完成状态的监听:

第一步:给AppDatabase新增预填充完成状态标记

abstract class AppDatabase : RoomDatabase() {
    // 预填充完成的状态标记
    val prePopulateComplete: CompletableDeferred<Unit> = CompletableDeferred()

    // 原有Dao声明等代码不变
    abstract fun puzzleDao(): PuzzleDao

    companion object {
        // 原有INSTANCE等代码不变
        @Volatile
        private var INSTANCE: AppDatabase? = null
        private val ioScope = CoroutineScope(Dispatchers.IO)

        fun getDatabase(context: Context): AppDatabase {
            val tempInstance = INSTANCE
            if (tempInstance == null) {
                synchronized(this) {
                    val instance = Room.databaseBuilder(
                        context.applicationContext, AppDatabase::class.java, "mydbname"
                    ).addCallback(AppDatabaseCallback(ioScope))
                     .fallbackToDestructiveMigration()
                     .build()
                    INSTANCE = instance
                    return instance
                }
            } else {
                return tempInstance
            }
        }
    }
}

第二步:修改Callback,在预填充完成后更新状态

private class AppDatabaseCallback(private val scope: CoroutineScope) : RoomDatabase.Callback() {
    override fun onCreate(db: SupportSQLiteDatabase) {
        super.onCreate(db)
        INSTANCE?.let { database ->
            scope.launch {
                runCatching {
                    val puzzleDao = database.puzzleDao()
                    prePopulateDatabase(puzzleDao)
                    // 预填充成功,标记完成
                    database.prePopulateComplete.complete(Unit)
                }.onFailure {
                    // 预填充失败,传递异常,上层可以捕获处理
                    database.prePopulateComplete.completeExceptionally(it)
                }
            }
        }
    }

    override fun onOpen(db: SupportSQLiteDatabase) {
        super.onOpen(db)
        INSTANCE?.let { database ->
            // 如果是已经存在的老数据库,不需要预填充,直接标记完成
            if (!database.prePopulateComplete.isCompleted) {
                database.prePopulateComplete.complete(Unit)
            }
        }
    }

    private suspend fun prePopulateDatabase(puzzleDao: PuzzleDao) {
        val startingData = StartingData()
        puzzleDao.insertAllPuzzles(startingData.getStartingPuzzles())
    }
}

第三步:修改Fragment的查询逻辑,等待预填充完成后再查询

loadPuzzlesJob = viewLifecycleOwner.lifecycleScope.launch(Dispatchers.IO) {
    val db = AppDatabase.getDatabase(requireContext())
    // 等待预填充完成,异常情况可以在这里加try-catch处理
    db.prePopulateComplete.await()

    val puzzleDao = db.puzzleDao()
    allPuzzles = puzzleDao.getAllPuzzles().toMutableList()

    if (allPuzzles.isEmpty()) {
        errorFound(ErrorType.PuzzlesNotFound)
    }
}

方案二:预填充完成后再返回数据库实例

如果你的预填充数据量很小,也可以直接把预填充逻辑放在数据库实例创建的同步流程中,确保返回实例时数据已经就绪,不需要单独写Callback:

abstract class AppDatabase : RoomDatabase() {
    abstract fun puzzleDao(): PuzzleDao

    companion object {
        @Volatile
        private var INSTANCE: AppDatabase? = null
        private val initLock = Mutex()

        suspend fun getDatabase(context: Context): AppDatabase {
            val tempInstance = INSTANCE
            if (tempInstance != null) return tempInstance
            // 用协程锁代替synchronized,支持挂起函数执行
            initLock.withLock {
                val instance = Room.databaseBuilder(
                    context.applicationContext, AppDatabase::class.java, "mydbname"
                ).fallbackToDestructiveMigration()
                 .build()
                // 预填充完成后再返回实例
                val startingData = StartingData()
                instance.puzzleDao().insertAllPuzzles(startingData.getStartingPuzzles())
                INSTANCE = instance
                return instance
            }
        }
    }
}

原有代码可优化点

  • 不要每次调用getDatabase都创建新的CoroutineScope,建议复用全局的IO Scope,避免内存泄漏
  • Fragment中启动协程建议使用viewLifecycleOwner.lifecycleScope,会在页面销毁时自动取消,不需要手动调用cancelAndJoin

内容的提问来源于stack exchange,提问作者brisdeveloper

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 01:54:00