使用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
相关产品推荐
相关产品推荐

