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

使用Dagger为ViewModel注入CoroutineScope时依赖报错如何解决

问题根源说明

第一类Dagger报错原因

  • 你自定义的@ApplicationScope限定符没有加到AppModule的Scope提供方法上,Dagger无法匹配ViewModel构造参数的限定符要求,找不到对应绑定
  • CoroutineScope()构造为空,没有传入合法的CoroutineContext参数,本身属于非法实例,就算Dagger绑定成功运行也会崩溃

第二类单元测试NPE原因

  • 不要用Mockito Mock CoroutineScope,Mock对象的coroutineContext属性没有真实实现,调用launch时访问该属性就会触发空指针
  • 代码硬编码Dispatchers.IO,测试时无法切换到同步测试调度器,会出现异步任务未执行完成就先走验证逻辑的问题,测试稳定性差
完整实现方案

1. 定义必要的限定符注解

用于区分不同用途的协程Scope和调度器,避免Dagger绑定冲突

// 全局Application生命周期协程Scope限定符
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class ApplicationScope

// IO调度器限定符
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class IoDispatcher

// 主线程调度器限定符
@Qualifier
@Retention(AnnotationRetention.RUNTIME)
annotation class MainDispatcher

2. 修正AppModule依赖提供逻辑

@Module
class AppModule {
    @Provides
    @ApplicationScope
    fun provideApplicationCoroutineScope(@MainDispatcher mainDispatcher: CoroutineDispatcher): CoroutineScope {
        // SupervisorJob隔离子协程异常,单个子协程失败不会导致整个全局Scope失效
        return CoroutineScope(SupervisorJob() + mainDispatcher)
    }

    @Provides
    @IoDispatcher
    fun provideIoDispatcher(): CoroutineDispatcher = Dispatchers.IO

    @Provides
    @MainDispatcher
    fun provideMainDispatcher(): CoroutineDispatcher = Dispatchers.Main.immediate
}

3. 修正ViewModel代码

移除硬编码的调度器,改为注入方式,适配测试切换需求

class MainActivityViewModel @Inject constructor(
    @ApplicationScope private val applicationScope: CoroutineScope,
    private val dataRepository: DataRepository,
    @IoDispatcher private val ioDispatcher: CoroutineDispatcher
): ViewModel() {
    val _charactersLiveData = MutableLiveData<Result<ArrayList<Character>>>()
    val charactersLiveData: LiveData<Result<ArrayList<Character>>> = _charactersLiveData

    fun fetchCharacters() {
        applicationScope.launch(ioDispatcher) {
            _charactersLiveData.postValue(dataRepository.getCharacters())
        }
    }

    // 可选优化:如果不需要跨页面的全局协程任务,可直接用viewModelScope+注入调度器,不用注入全局Scope
    // 示例:
    // fun fetchCharacters() {
    //     viewModelScope.launch(ioDispatcher) {
    //         _charactersLiveData.postValue(dataRepository.getCharacters())
    //     }
    // }
}

注意:如果使用viewModelScope的实现方案,不需要再注入CoroutineScope,ViewModel销毁时会自动取消所有未完成的子协程,不会出现内存泄漏问题,更推荐该实现。

4. 单元测试实现

引入kotlinx-coroutines-test测试依赖,使用TestScope替代Mock的CoroutineScope,通过测试规则替换全局调度器实现同步执行

4.1 定义协程测试规则

@OptIn(ExperimentalCoroutinesApi::class)
class TestCoroutineRule : TestWatcher() {
    val testDispatcher = StandardTestDispatcher()
    val testScope = TestScope(testDispatcher)

    override fun starting(description: Description?) {
        super.starting(description)
        Dispatchers.setMain(testDispatcher)
    }

    override fun finished(description: Description?) {
        super.finished(description)
        Dispatchers.resetMain()
    }
}

4.2 修正测试代码

@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(MockitoJUnitRunner::class)
class MainActivityViewModelTest {
    @get:Rule
    val testCoroutineRule = TestCoroutineRule()

    @Mock
    private lateinit var dataRepository: DataRepository
    @Mock
    private lateinit var dataObserver: Observer<Result<ArrayList<Character>>>

    private lateinit var mainActivityViewModel: MainActivityViewModel

    @Before
    fun setUp() {
        // 手动构造ViewModel,传入测试Scope和测试调度器
        mainActivityViewModel = MainActivityViewModel(
            applicationScope = testCoroutineRule.testScope,
            dataRepository = dataRepository,
            ioDispatcher = testCoroutineRule.testDispatcher
        )
        mainActivityViewModel.charactersLiveData.observeForever(dataObserver)
    }

    @Test
    fun fetchCharacters() = testCoroutineRule.testScope.runTest {
        val testCharacters = arrayListOf(Character(
            name = "myName",
            img = "image",
            occupation = arrayListOf(),
            status = "status",
            nickname = "nickName",
            appearance = arrayListOf()
        ))
        Mockito.`when`(dataRepository.getCharacters()).thenReturn(Result.success(testCharacters))

        mainActivityViewModel.fetchCharacters()
        // 等待所有协程任务执行完成
        testCoroutineRule.testScheduler.advanceUntilIdle()

        Mockito.verify(dataRepository).getCharacters()
        Mockito.verify(dataObserver).onChanged(Result.success(testCharacters))
    }

    @After
    fun tearDown() {
        mainActivityViewModel.charactersLiveData.removeObserver(dataObserver)
    }
}

内容的提问来源于stack exchange,提问作者BRDroid

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.05 14:24:01