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

使用SomeUseCase更新Room数据库后SharedFlow无响应的解决方法

Room+Hilt集成问题:使用UseCase更新数据库后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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 19:20:14