WorkManager 与 Hilt/Dagger 依赖注入

依赖注入这个话题,说实话在 Android 开发里已经不算新鲜了。但 WorkManager 和 Hilt 怎么配合,很多人一开始都会踩坑。我记得刚接触 WorkManager 时,第一反应就是:Worker 的构造函数不是我能控制的,那怎么把 Repository 传进去?

嗯,今天我们就来彻底解决这个问题。

为什么 Worker 需要依赖注入?

先看一个最朴素的场景。你有一个后台任务,需要从网络拉数据,然后存到本地数据库。如果没有依赖注入,你可能会这样写:

class SyncWorker(
    context: Context,
    params: WorkerParameters
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        // 直接在 Worker 里创建依赖
        val api = Retrofit.Builder()
            .baseUrl("https://api.example.com")
            .build()
            .create(ApiService::class.java)

        val dao = AppDatabase.getInstance(applicationContext).userDao()

        // 业务逻辑...
        return Result.success()
    }
}

这样写有什么问题?我告诉你,问题大了去了。首先,测试变得极其困难——你没法 mock 掉 api 和 dao。其次,如果多个 Worker 都需要同样的依赖,你就得重复创建。说白了,这违反了依赖反转原则。

核心问题:Worker 的构造函数由 WorkManager 框架控制,你不能像 Activity 或 Fragment 那样通过构造函数注入。

HiltWorker 注解:最简单的注入方式

Hilt 从 1.0 版本开始就提供了 @HiltWorker 注解。我个人习惯用这种方式,因为它最省事。

用法很简单,三步走:

  1. 给 Worker 类加上 @HiltWorker 注解
  2. 在构造函数里用 @Inject 标注需要注入的参数
  3. 确保 Application 类用 @HiltAndroidApp 注解
@HiltWorker
class SyncWorker @Inject constructor(
    context: Context,
    workerParams: WorkerParameters,
    private val userRepository: UserRepository
) : CoroutineWorker(context, workerParams) {

    override suspend fun doWork(): Result {
        return try {
            userRepository.syncUsers()
            Result.success()
        } catch (e: Exception) {
            Result.retry()
        }
    }
}

你看,这样写就清爽多了。UserRepository 由 Hilt 自动提供,你完全不用操心它的生命周期。

小提示:@HiltWorker 注解会为 Worker 生成一个 HiltWorkerFactory,这个工厂类会自动注入到 WorkManager 的配置中。但前提是你的 Application 类必须用 @HiltAndroidApp 注解。

WorkerFactory 的创建:更灵活的控制

如果你不想用 @HiltWorker,或者你需要更细粒度的控制,可以自己实现 WorkerFactory。我在项目中遇到过一种情况:同一个 Worker 类需要根据不同的参数使用不同的依赖实例。这时候 @HiltWorker 就有点力不从心了。

自己写 WorkerFactory 其实不复杂:

class HiltWorkerFactory @Inject constructor(
    private val userRepository: UserRepository,
    private val orderRepository: OrderRepository
) : WorkerFactory() {

    override fun createWorker(
        appContext: Context,
        workerClassName: String,
        workerParameters: WorkerParameters
    ): ListenableWorker? {
        return when (workerClassName) {
            SyncWorker::class.java.name -> {
                SyncWorker(appContext, workerParameters, userRepository)
            }
            OrderSyncWorker::class.java.name -> {
                OrderSyncWorker(appContext, workerParameters, orderRepository)
            }
            else -> null
        }
    }
}

然后你需要在 Application 里配置这个工厂:

@HiltAndroidApp
class MyApplication : Application(), Configuration.Provider {

    @Inject
    lateinit var hiltWorkerFactory: HiltWorkerFactory

    override val workManagerConfiguration: Configuration
        get() = Configuration.Builder()
            .setWorkerFactory(hiltWorkerFactory)
            .build()
}
注意:如果你同时使用了 @HiltWorker 和自定义 WorkerFactory,@HiltWorker 生成的工厂会被你自定义的工厂覆盖。两者只能选其一,不要混用。

注入 Repository 到 Worker 中

不管用哪种方式,最终目的都是把 Repository 注入到 Worker 里。这里我建议遵循一个原则:Worker 只负责调度和生命周期管理,业务逻辑全部委托给 Repository

举个例子,假设你有一个 UserRepository:

class UserRepository @Inject constructor(
    private val apiService: ApiService,
    private val userDao: UserDao
) {
    suspend fun syncUsers(): List<User> {
        val users = apiService.fetchUsers()
        userDao.insertAll(users)
        return users
    }
}

那么 Worker 里的代码就变得非常薄:

@HiltWorker
class SyncWorker @Inject constructor(
    context: Context,
    params: WorkerParameters,
    private val userRepository: UserRepository
) : CoroutineWorker(context, params) {

    override suspend fun doWork(): Result {
        return userRepository.syncUsers().let {
            Result.success()
        }
    }
}

这样做的好处很明显:测试 Worker 时,你只需要 mock 掉 UserRepository,不需要关心它内部是怎么调用 API 和数据库的。

测试注入的 Worker

测试这块,说实话很多人会忽略。但我觉得,后台任务一旦出问题,排查起来比 UI 层麻烦得多。所以测试一定要跟上。

用 Hilt 的测试框架,你可以这样写:

@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class SyncWorkerTest {

    @get:Rule
    val hiltRule = HiltAndroidRule(this)

    @Inject
    lateinit var userRepository: UserRepository

    @Before
    fun setup() {
        hiltRule.inject()
    }

    @Test
    fun testSyncWorker_success() = runBlocking {
        // 准备测试数据
        val context = ApplicationProvider.getApplicationContext<Context>()
        val workerParams = WorkerParameters(
            UUID.randomUUID(),
            Data.EMPTY,
            emptyList(),
            WorkerParameters.RuntimeExtras(),
            1,
            1,
            context.resources,
            null,
            null,
            null
        )

        // 创建 Worker 实例
        val worker = SyncWorker(context, workerParams, userRepository)

        // 执行任务
        val result = worker.doWork()

        // 验证结果
        assertEquals(Result.success(), result)
    }
}

我曾经踩过一个坑:测试时忘记注入依赖,结果 userRepository 一直是 null。后来我养成了一个习惯——每次写测试前,先确认 hiltRule.inject() 有没有调用。

测试小技巧:如果你用 Mockito 或 MockK,可以在测试中替换真实的 Repository 为 mock 对象。这样你就能精确控制 Worker 的输入输出了。

知识体系总览

下面这张图总结了 WorkManager 与 Hilt 依赖注入的核心流程:

WorkManager + Hilt 依赖注入流程 @HiltAndroidApp HiltWorkerFactory / @HiltWorker SyncWorker @Inject constructor UserRepository @Inject 关键点 • Worker 构造函数由框架控制 • @HiltWorker 自动生成工厂 • 自定义工厂更灵活 • 业务逻辑委托给 Repository • 测试时注入 mock 依赖 • 避免在 Worker 中直接创建依赖 • 遵循单一职责原则 • 注意工厂覆盖问题

两种注入方式对比

对比维度 @HiltWorker 注解 自定义 WorkerFactory
代码量 少,只需一个注解 多,需要实现工厂类
灵活性 中等,依赖由 Hilt 自动提供 高,可以手动控制依赖实例
适用场景 大多数常规场景 需要根据参数选择不同依赖
测试难度 容易,Hilt 测试框架支持 中等,需要手动 mock 工厂
维护成本 低,Hilt 自动管理 中,工厂代码需要维护

我个人建议,除非你有特殊需求,否则优先用 @HiltWorker。它简单、可靠,而且 Hilt 团队一直在维护。我刚开始用自定义工厂时,就遇到过因为工厂里忘记处理某个 Worker 类型,导致运行时崩溃的问题。后来换成 @HiltWorker,这类问题再也没出现过。

好了,关于 WorkManager 和 Hilt 的依赖注入,核心内容就是这些。记住一点:让 Worker 保持轻量,把业务逻辑交给 Repository,用依赖注入来解耦。这样你的代码不仅好测,也好维护。

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