Kotlin异步操作等待问题:Token刷新后页面跳转受阻
问题解决与最佳实践
核心问题分析
你当前代码里的refreshAccessToken使用了runBlocking(Dispatchers.IO),这会直接阻塞调用线程——而Activity的lifecycleScope默认运行在主线程,调用这个函数会导致Splash界面卡住无响应。同时现有写法无法感知异步操作的完成时机,自然做不到完成后再执行页面跳转。
解决方案代码修改
1. 重构MyViewModel.kt
移除runBlocking,改用ViewModel自带的viewModelScope(它会在ViewModel销毁时自动取消协程,避免内存泄漏),并将refreshAccessToken改为挂起函数,让调用方可以等待操作完成:
@HiltViewModel class SplashScreenViewModel @Inject constructor( private val tokenManager: TokenManager ) : ViewModel() { private val _state = MutableStateFlow(SplashScreenState()) val state: StateFlow<SplashScreenState> = _state.asStateFlow() // 改为挂起函数,移除runBlocking suspend fun refreshAccessToken(refreshToken: String) { try { val loggingInterceptor = HttpLoggingInterceptor() loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY val okHttpClient = OkHttpClient .Builder() .addInterceptor(loggingInterceptor) //.authenticator(AuthAuthenticator(tokenManager)) .build() val retrofit = Retrofit.Builder() .baseUrl(Constants.BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .client(okHttpClient) .build() val service = retrofit.create(RegisterService::class.java) val response = service.refreshToken(model = RefreshToken(refreshToken)) if (response.isSuccessful && response.body() != null) { val newAccessToken = response.body()?.access_token val newRefreshToken = response.body()?.refresh_token if (newAccessToken != null && newRefreshToken != null) { tokenManager.saveAccessToken(newAccessToken) tokenManager.saveRefreshToken(newRefreshToken) } } } catch (e: Exception) { _state.update { it.copy( error = true ) } } } } data class SplashScreenState( val error: Boolean = false, )
2. 修改SplashActivity.kt
- 调用
refreshAccessToken时等待操作完成,再执行跳转 - 监听ViewModel的
state变化,错误时显示提示 - 优化协程逻辑,用
first()代替collect获取单次token(如果TokenManager返回的是Flow)
@AndroidEntryPoint @SuppressLint("CustomSplashScreen") class SplashActivity : ComponentActivity() { private val viewModel: SplashScreenViewModel by viewModels() private val tokenManager = TokenManager(this) private val errMsg = "Bir hata ile karşılaşıldı." fun navigateToMain(isTokenExist: Boolean) { val intent = Intent(this@SplashActivity, MainActivity::class.java) intent.putExtra("isTokenExist", isTokenExist) startActivity(intent) finish() } private suspend fun handleSplashLogic() { // 仅获取一次refreshToken,用first()替代collect val refreshToken = tokenManager.getRefreshToken().first() if (refreshToken != null) { // 等待token刷新完成 viewModel.refreshAccessToken(refreshToken) // 检查是否出现错误 if (viewModel.state.value.error) { Toast.makeText(this@SplashActivity, errMsg, Toast.LENGTH_LONG).show() } navigateToMain(true) } else { navigateToMain(false) } } override fun onCreate(savedInstanceState: Bundle?) { var keepSplashOn = true if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val splashScreen = installSplashScreen() splashScreen.setKeepOnScreenCondition { keepSplashOn } } super.onCreate(savedInstanceState) // 监听状态中的错误变化 lifecycleScope.launch { viewModel.state.collect { state -> if (state.error) { Toast.makeText(this@SplashActivity, errMsg, Toast.LENGTH_LONG).show() } } } // 执行启动逻辑,完成后关闭闪屏 lifecycleScope.launch { handleSplashLogic() keepSplashOn = false } } }
该场景的最佳实践
- ViewModel协程用viewModelScope:ViewModel自带的
viewModelScope会在ViewModel销毁时自动取消所有协程,避免内存泄漏,绝对不要在ViewModel中使用runBlocking阻塞线程。 - 异步操作用挂起函数/Flow:让ViewModel的异步方法返回挂起函数或Flow,调用方(Activity/Fragment)通过协程等待或监听结果,而非阻塞线程。
- SplashScreen正确用法:Android 12+的闪屏可以通过
setKeepOnScreenCondition控制显示时长,完成逻辑后将条件设为false,让系统自动关闭闪屏,避免手动控制的繁琐。 - 主线程只做UI操作:所有网络IO、本地存储操作必须放在IO协程调度器中,主线程仅处理UI更新。
- 状态统一管理:用StateFlow管理ViewModel状态,Activity/Fragment通过collect监听状态变化,实现UI与业务逻辑解耦。
内容的提问来源于stack exchange,提问作者NewPartizal
相关产品推荐
相关产品推荐

