使用SomeUseCase更新Room数据库后SharedFlow无响应的解决方法
问题背景
我开发的应用集成了Room与Hilt(所有模块均绑定SingletonComponent::class),包含两个核心接口:
- SourceOfData:订阅Room的Flow并转换成供多个ViewModel共享的SharedFlow
- SomeUseCase:负责执行数据库的插入、删除等变更操作
核心代码
接口定义
interface SomeUseCase { suspend fun addPerson(person: Person) suspend fun deletePersonById(personId: Int) suspend fun addItemByPersonId(personId: Int, item: Item) suspend fun deleteItemById(itemId: Int) } interface SourceOfData { fun personSharedFlowForMap(): SharedFlow<List<Person>> // 测试用操作方法 suspend fun addPerson(person: Person) suspend fun deletePersonById(personId: Int) suspend fun addItemByPersonId(personId: Int, item: Item) suspend fun deleteItemById(itemId: Int) }
SourceOfData实现类
class SourceOfDataImpl @Inject constructor( private val personDao: PersonDao, private val itemDao: ItemDao ) : SourceOfData { private val _personFlowMap = MutableSharedFlow<List<Person>>() val personFlowMap: SharedFlow<List<Person>> = _personFlowMap.asSharedFlow() init { subscribeToPersonMapWithItemDao() } private fun subscribeToPersonMapWithItemDao() { CoroutineScope(Dispatchers.IO).launch { combine( personDao.getPersons(), itemDao.getItems() ) { personEntityList, itemEntityList -> personEntityList.map { personEntity -> Person( personEntity.id, personEntity.name, itemEntityList .filter { it.personId == personEntity.id } .map { it.toItem() } ) } }.collect { _personFlowMap.emit(it) } } } // 省略接口中测试方法的实现(直接调用Dao操作数据库) override suspend fun addPerson(person: Person) { personDao.insert(person.toEntity()) } // ...其他方法实现 }
BaseViewModel
abstract class BaseViewModel(private val sourceOfData: SourceOfData) : ViewModel() { abstract fun updateUiStateMap(personUiMap: PersonUiMap) fun subscribeToDataMap() { viewModelScope.launch { sourceOfData.personSharedFlowForMap().collect { val personList = it updateUiStateMap(convertDataListToUiMap(personList)) } } } }
MainViewModel中的测试代码
//TODO("Example") suspend fun addPerson(personName: String) { // someUseCase.addPerson(Person(name = personName)) //TODO("无响应") // sourceOfData.addPerson(Person(name = personName)) //TODO("正常触发更新") }
问题描述
调用SomeUseCase的方法更新数据库时,数据库数据确实已变更,但SourceOfData中的SharedFlow没有发射新值,导致UI无法更新;而直接调用SourceOfData的操作方法时,SharedFlow能正常响应并触发UI更新。
原因分析
最可能的原因是数据库实例不唯一:
Hilt中如果没有将Room的Database类标记为@Singleton,即使模块绑定到SingletonComponent,每次注入Database或其Dao时都会创建新的实例。这会导致:
SourceOfDataImpl使用的Dao实例和SomeUseCase使用的Dao实例操作的是不同的数据库文件- 当
SomeUseCase更新数据库时,SourceOfDataImpl监听的是另一个数据库,自然不会收到变更通知
另外,也有可能是SourceOfDataImpl中用于collect Room Flow的CoroutineScope被意外取消,但这种情况不会出现“仅SourceOfData操作有效”的现象,因此优先级较低。
解决方案
方案1:确保Database为单例(最优)
在你的Room Database类上添加@Singleton注解,并在Hilt模块中正确提供单例的Database实例:
@Singleton @Database(entities = [PersonEntity::class, ItemEntity::class], version = 1) abstract class AppDatabase : RoomDatabase() { abstract fun personDao(): PersonDao abstract fun itemDao(): ItemDao } // Hilt模块提供Database实例 @Module @InstallIn(SingletonComponent::class) object DatabaseModule { @Provides @Singleton fun provideAppDatabase(@ApplicationContext context: Context): AppDatabase { return Room.databaseBuilder( context, AppDatabase::class.java, "app_database" ).build() } }
这样所有依赖Dao的类(SourceOfDataImpl和SomeUseCase)都会获取到同一个Dao实例,操作的是同一个数据库,Room的Flow就能在数据库变更时正常发射新值,最终触发SharedFlow更新。
方案2:让SomeUseCase依赖SourceOfData
如果不想调整Database的注入方式,可以修改SomeUseCase的实现,使其依赖SourceOfData而非直接依赖Dao,所有数据库操作都通过SourceOfData执行:
class SomeUseCaseImpl @Inject constructor( private val sourceOfData: SourceOfData ) : SomeUseCase { override suspend fun addPerson(person: Person) { sourceOfData.addPerson(person) } override suspend fun deletePersonById(personId: Int) { sourceOfData.deletePersonById(personId) } // 其他方法同理 }
这种方式能保证所有数据库操作都通过同一个Dao实例执行,避免数据库实例不一致的问题,但会让SomeUseCase和SourceOfData产生耦合,适合不想修改Database注入逻辑的场景。
总结
优先选择方案1,通过确保Database单例来从根源解决数据库实例不一致的问题,这符合Hilt和Room的最佳实践,也能保持代码的低耦合性。
内容的提问来源于stack exchange,提问作者Aleksei Laptev

