如何在Hilt中注入来自挂起函数的依赖?(加密Room数据库场景)
在Dagger Hilt模块中调用挂起函数获取Room加密密钥的解决方案
问题分析
Hilt的@Provides方法是同步执行的,无法直接调用挂起函数,而你的密钥获取逻辑因为依赖DataStore的挂起API必须写成挂起函数。直接用runBlocking确实存在隐患:它会阻塞当前线程,如果在主线程执行,极有可能引发ANR;即使在后台线程,也会占用线程资源,违背协程非阻塞的设计初衷。
可行替代方案
方案1:提供数据库的Flow实例,延迟初始化
将数据库的创建逻辑包装成Flow,依赖方通过收集Flow获取数据库实例,全程非阻塞:
@InstallIn(SingletonComponent::class) @Module class FileDataBaseModule { @Provides @Singleton fun provideChannelDao(fileDataBase: FileDataBase): MyFileDao { return fileDataBase.myFileDao() } @Provides @Singleton fun provideAppDatabaseFlow( @ApplicationContext context: Context, databaseKeyStorage: DatabaseKeyStorage, @IoDispatcher dispatcher: CoroutineDispatcher ): Flow<FileDataBase> = flow { val dbKey = databaseKeyStorage.getDbKey() emit(FileDataBase.create(context, dbKey)) }.flowOn(dispatcher).shareIn( CoroutineScope(SupervisorJob() + dispatcher), SharingStarted.Eagerly, replay = 1 ) }
需要先定义@IoDispatcher限定符:
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class IoDispatcher @InstallIn(SingletonComponent::class) @Module object DispatcherModule { @Provides @IoDispatcher fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO }
在依赖数据库的类中,通过挂起函数获取实例:
class MyRepository @Inject constructor( private val dbFlow: Flow<FileDataBase> ) { suspend fun getMyFileDao(): MyFileDao { return dbFlow.first().myFileDao() } }
优点:完全遵循协程非阻塞设计,避免线程阻塞;shareIn确保数据库只初始化一次。
缺点:所有依赖数据库的地方都需要适配挂起调用或Flow收集。
方案2:封装数据库提供者类,延迟初始化
创建一个专门的类负责数据库的延迟初始化,内部处理挂起逻辑:
class DatabaseProvider @Inject constructor( @ApplicationContext private val context: Context, private val databaseKeyStorage: DatabaseKeyStorage ) { @Volatile private var dbInstance: FileDataBase? = null suspend fun getDatabase(): FileDataBase { return dbInstance ?: synchronized(this) { dbInstance ?: run { val dbKey = databaseKeyStorage.getDbKey() val newDb = FileDataBase.create(context, dbKey) dbInstance = newDb newDb } } } }
然后在Hilt模块中提供这个提供者:
@Provides @Singleton fun provideDatabaseProvider( @ApplicationContext context: Context, databaseKeyStorage: DatabaseKeyStorage ): DatabaseProvider { return DatabaseProvider(context, databaseKeyStorage) }
在需要DAO的地方注入DatabaseProvider并调用挂起方法:
class MyViewModel @Inject constructor( private val dbProvider: DatabaseProvider ) : ViewModel() { suspend fun fetchFiles() { val dao = dbProvider.getDatabase().myFileDao() // 执行数据库操作 } }
优点:集中管理数据库初始化逻辑,依赖方只需调用挂起方法,无需关心底层实现。
缺点:同样需要依赖方适配挂起调用。
方案3:优化runBlocking的使用(权宜之计)
如果必须保持同步的@Provides方法,可以将runBlocking绑定到IO线程,避免阻塞主线程:
@Provides @Singleton fun provideAppDatabase( @ApplicationContext context: Context, databaseKeyStorage: DatabaseKeyStorage, @IoDispatcher dispatcher: CoroutineDispatcher ): FileDataBase { val dbKey = runBlocking(dispatcher) { databaseKeyStorage.getDbKey() } return FileDataBase.create(context, dbKey) }
注意:同样需要使用上面定义的@IoDispatcher限定符。
优点:无需修改现有依赖逻辑,快速适配。
缺点:仍然存在线程阻塞,只是转移到了IO线程,若密钥生成或DataStore读写耗时较长,会延迟数据库初始化完成时间。
总结
- 优先选择方案1或方案2,它们符合协程的非阻塞设计,是更健壮的长期解决方案。
- 若项目架构暂时无法适配挂起调用,可使用方案3作为过渡,但需注意线程阻塞的潜在影响。
内容的提问来源于stack exchange,提问作者Denis Sonozaki
相关产品推荐
相关产品推荐

