viewModelScope.launch运行异常问题咨询(Dagger-Hilt场景)
问题描述
示例1
首次打开主Activity时,MainActivityViewModel中的viewModelScope.launch运行正常;但打开第二个Activity并关闭主Activity后,再次打开主Activity时,viewModelScope.launch因viewModelScope未激活而无法运行。
示例2
首次打开主Activity时,viewModelScope.launch运行正常;但打开第二个Activity且不关闭主Activity,再次打开主Activity时,viewModelScope.launch会继续之前的任务同时启动新任务。
技术栈
使用Dagger-Hilt进行依赖注入
需求
- 实现每次打开主Activity时都能启动
viewModelScope.launch,并找出示例1的问题原因; - 在打开第二个Activity且不关闭主Activity时,停止
viewModelScope.launch的运行。
解决方案
问题1:示例1原因分析与修复
原因
调用finish()关闭MainActivity时,ViewModel会被系统销毁,其viewModelScope也会被标记为非激活状态。若出现重建后协程无法启动的情况,核心原因是ViewModel实例未被正确重建——可能是Hilt配置错误将ViewModel设为单例,或手动使用ViewModelProvider时未正确关联Hilt的Factory,导致复用了已销毁的ViewModel实例。
修复步骤
- 检查Hilt配置,确保
@HiltViewModel注解的MainActivityViewModel未被错误绑定为单例。 - 在Activity中使用Hilt提供的ViewModelFactory获取实例,替代手动无参
ViewModelProvider:
@AndroidEntryPoint public class MainActivity extends AppCompatActivity { private MainActivityViewModel viewModel; ... @Override protected void onCreate(Bundle savedInstanceState) { ... // 正确使用Hilt的ViewModelFactory viewModel = new ViewModelProvider(this, DefaultViewModelProviderFactory.getInstance(this)) .get(MainActivityViewModel.class); viewModel.testOneViewModelScopeMA(); binding.btnSecondActivity.setOnClickListener(view -> startSecondActivityIntentWithFinishCurrentActivity(MainActivity.this) ); ... } private void startSecondActivityIntentWithFinishCurrentActivity(Context _context) { Intent intent = new Intent(_context, SecondActivity.class); _context.startActivity(intent); finish(); } }
- 在ViewModel方法中添加激活状态判断,确保协程仅在有效状态下启动:
fun testOneViewModelScopeMA() { Timber.d("-> testOneViewModelScope, \nviewModelScope is active: ${viewModelScope.isActive}") if (viewModelScope.isActive) { viewModelScope.launch { try { var count = 0 while (true) { delay(1000L) count += 1 Timber.d( "-> testOneViewModelScope, " + "\nviewModelScope is active: ${viewModelScope.isActive} " + "\noutput: $count " ) } } catch (e: CancellationException) { Timber.e( "-> testOneViewModelScope, " + "\nviewModelScope is active: ${viewModelScope.isActive} " + "\nCancellationException: \n${e.localizedMessage}" ) throw e } catch (ex: Exception) { Timber.e( "-> testOneViewModelScope, " + "\nviewModelScope is active: ${viewModelScope.isActive} " + "\nException: \n${ex.localizedMessage}" ) } } } else { Timber.w("-> testOneViewModelScope: viewModelScope is not active, cannot launch coroutine") } }
问题2:示例2的修复(切换Activity时停止协程)
思路
通过保存协程的Job引用,在Activity进入后台时主动取消任务,避免重复启动或后台运行。
修复步骤
- 在ViewModel中维护协程Job引用,并提供取消方法:
@HiltViewModel class MainActivityViewModel @Inject constructor( private val mainActivityRepository: MainActivityRepository, @IoDispatcher private val ioDispatcher: CoroutineDispatcher, @MainDispatcher private val mainDispatcher: CoroutineDispatcher, @DefaultDispatcher private val defaultDispatcher: CoroutineDispatcher ) : ViewModel() { private var coroutineJob: Job? = null fun testOneViewModelScopeMA() { // 先取消已有任务,避免重复启动 coroutineJob?.cancel() Timber.d("-> testOneViewModelScope, \nviewModelScope is active: ${viewModelScope.isActive}") coroutineJob = viewModelScope.launch { try { var count = 0 while (true) { delay(1000L) count += 1 Timber.d( "-> testOneViewModelScope, " + "\nviewModelScope is active: ${viewModelScope.isActive} " + "\noutput: $count " ) } } catch (e: CancellationException) { Timber.e( "-> testOneViewModelScope, " + "\nviewModelScope is active: ${viewModelScope.isActive} " + "\nCancellationException: \n${e.localizedMessage}" ) throw e } catch (ex: Exception) { Timber.e( "-> testOneViewModelScope, " + "\nviewModelScope is active: ${viewModelScope.isActive} " + "\nException: \n${ex.localizedMessage}" ) } finally { coroutineJob = null } } } // 供Activity调用,取消运行中的协程 fun cancelRunningCoroutine() { coroutineJob?.cancel() coroutineJob = null } }
- 在Activity的
onStop方法中调用取消逻辑:
@AndroidEntryPoint public class MainActivity extends AppCompatActivity { private MainActivityViewModel viewModel; ... @Override protected void onStop() { super.onStop(); // Activity进入后台时取消协程 viewModel.cancelRunningCoroutine(); } @Override protected void onCreate(Bundle savedInstanceState) { ... viewModel = new ViewModelProvider(this, DefaultViewModelProviderFactory.getInstance(this)) .get(MainActivityViewModel.class); viewModel.testOneViewModelScopeMA(); binding.btnSecondActivity.setOnClickListener(view -> startSecondActivityIntentWithOutFinishCurrentActivity(MainActivity.this) ); ... } private void startSecondActivityIntentWithOutFinishCurrentActivity(Context _context) { Intent intent = new Intent(_context, SecondActivity.class); _context.startActivity(intent); } }
补充说明
- 若需Activity回到前台时自动重启协程,可在
onResume方法中再次调用viewModel.testOneViewModelScopeMA()。 viewModelScope会在ViewModel销毁时自动取消所有协程,无需额外处理内存泄漏。
内容的提问来源于stack exchange,提问作者DevPeter
相关产品推荐
相关产品推荐

