12. Hilt与Room:数据库模块的封装、DAO与Database的注入

说实话,在Android开发里,Room和Hilt的组合,是我见过最舒服的「数据层+依赖注入」搭档。一个管数据持久化,一个管对象创建和生命周期。但舒服归舒服,封装不好照样翻车。

我记得刚接手一个老项目时,发现每个Activity里都new了一个Room.databaseBuilder(...)。你想想看,这要是换了数据库版本,得改多少个地方?更别提测试了,根本没法mock。所以今天我们就来聊聊,怎么用Hilt把Room模块封装得漂漂亮亮。

12.1 为什么需要封装数据库模块?

直接回答:为了解耦和可测试性。

Room的Database对象,说白了就是一个重量级单例。它需要Application Context,需要做Migration,还得保证整个App里只有一个实例。这些活儿,交给Hilt来干最合适。

我个人习惯把数据库相关的依赖分成三层:

  • Database层:提供RoomDatabase实例
  • DAO层:提供具体的数据访问对象
  • Repository层:组合多个DAO,对外暴露业务数据

Hilt要做的,就是把这每一层的对象都管理起来。你不需要关心它们什么时候创建、什么时候销毁,只需要声明「我需要什么」就行。

核心原则: Database和DAO应该是单例的。Repository可以是单例,也可以按需创建,取决于你的业务场景。

12.2 定义Database模块

我们先写一个标准的Room Database。假设我们有一个用户表和一个订单表:

@Database(
    entities = [UserEntity::class, OrderEntity::class],
    version = 1,
    exportSchema = true
)
abstract class AppDatabase : RoomDatabase() {
    abstract fun userDao(): UserDao
    abstract fun orderDao(): OrderDao
}

嗯,这里要注意:exportSchema = true 会生成数据库Schema文件,方便你做版本升级时参考。我建议一直开着,反正不影响运行。

接下来,我们用Hilt的Module来提供这个Database实例:

@Module
@InstallIn(SingletonComponent::class)
object DatabaseModule {

    @Provides
    @Singleton
    fun provideAppDatabase(
        @ApplicationContext context: Context
    ): AppDatabase {
        return Room.databaseBuilder(
            context,
            AppDatabase::class.java,
            "my_app.db"
        )
            .fallbackToDestructiveMigration()
            .build()
    }
}

这里有几个关键点:

  • @InstallIn(SingletonComponent::class):表示这个Module里的依赖是全局单例。因为Database本身就应该只有一个实例。
  • @Singleton:确保AppDatabase在整个App生命周期内只创建一次。
  • @ApplicationContext:Hilt会自动注入Application级别的Context,不需要你自己传。
避坑指南: 我曾经在生产环境里用过 .fallbackToDestructiveMigration(),结果数据库升级时用户数据全丢了。这个方法是「毁灭性回退」,只适合开发阶段。生产环境一定要写Migration。

12.3 注入DAO对象

Database有了,接下来就是DAO。很多人会直接在Module里再写一个provide方法:

@Provides
@Singleton
fun provideUserDao(database: AppDatabase): UserDao {
    return database.userDao()
}

这样写没问题,但有点啰嗦。如果我有10个DAO,就得写10个provide方法。我个人更推荐用@Binds的方式,或者干脆让Hilt自动注入——前提是你的DAO是接口,并且Database里已经声明了抽象方法。

其实更简洁的做法是:直接在需要DAO的地方注入Database,然后调用方法。但这样耦合度高,不推荐。

我通常的做法是:在Module里统一提供DAO,这样测试时可以轻松替换:

@Module
@InstallIn(SingletonComponent::class)
object DaoModule {

    @Provides
    @Singleton
    fun provideUserDao(database: AppDatabase): UserDao {
        return database.userDao()
    }

    @Provides
    @Singleton
    fun provideOrderDao(database: AppDatabase): OrderDao {
        return database.orderDao()
    }
}

你看,每个DAO的提供方法都依赖AppDatabase。Hilt会自动把之前创建的Database实例传进来。这就是依赖注入的魅力——你只管声明,框架帮你串联。

12.4 在ViewModel中使用

现在DAO已经可以注入了。我们在ViewModel里怎么用?

@HiltViewModel
class UserViewModel @Inject constructor(
    private val userDao: UserDao
) : ViewModel() {

    val users: LiveData<List<UserEntity>> = userDao.getAllUsers()

    fun addUser(name: String, age: Int) {
        viewModelScope.launch {
            userDao.insert(UserEntity(name = name, age = age))
        }
    }
}

注意看:@Inject constructor 告诉Hilt,这个ViewModel的构造参数需要注入。Hilt会从Module里找到UserDao的实例,然后传进来。

你可能会问:那Database呢?不需要了。因为DAO已经封装了所有数据库操作,ViewModel不应该直接接触Database。这是分层设计的基本要求。

小技巧: 如果你在ViewModel里直接注入Database,说明你的架构设计可能有问题。DAO就是Database的代理,ViewModel只跟DAO打交道。

12.5 整体架构图

下面这张图展示了Hilt、Room、DAO和ViewModel之间的依赖关系。我画的时候特意把箭头方向标清楚了,方便你理解谁依赖谁。

Hilt + Room 依赖注入架构图 Hilt Module AppDatabase UserDao OrderDao 其他DAO UserViewModel OrderViewModel @Provides @Singleton userDao() orderDao() 其他DAO() @Inject @Inject Hilt Module Database DAO ViewModel

从图里可以清楚看到:Hilt Module负责创建Database,Database负责提供DAO,ViewModel只依赖DAO。每一层职责清晰,替换起来也方便。

12.6 测试时的替换技巧

说到测试,这才是依赖注入的真正价值所在。假设你想测试UserViewModel,但不想真的操作数据库。怎么办?

很简单,在测试Module里提供一个假的UserDao:

@Module
@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [DaoModule::class]
)
object TestDaoModule {

    @Provides
    @Singleton
    fun provideUserDao(): UserDao {
        return FakeUserDao()  // 内存实现,不操作真实数据库
    }
}

这样,测试时Hilt会自动用FakeUserDao替换真实的UserDao。你的ViewModel代码一行都不用改。这就是依赖注入的「可插拔」特性。

总结一下: 用Hilt管理Room模块,核心就是三步——定义Database、提供DAO、注入到ViewModel。每一步都遵循单一职责原则,让代码更干净、更可测。

好了,这一章的内容就到这里。记住:好的架构不是一蹴而就的,而是在一次次重构中打磨出来的。你现在的代码,就是未来自己的「遗产」——写得好,未来感谢你;写得烂,未来骂死你。


公众号:蓝海资料掘金营,微信deep3321