如何确保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
相关产品推荐
相关产品推荐

