Compose Kotlin中SplashActivity ViewModel异步任务等待问题排查
解决启动页异步刷新Token后导航逻辑错误的问题
问题核心
当前代码中,SplashActivity检测到存在refreshToken后,调用refreshAccessToken的同时立刻跳转至MainActivity,完全未等待Token刷新异步任务完成。加上ViewModel里用runBlocking阻塞线程的写法不规范,且无法让Activity感知刷新完成状态,导致导航时新Token还未保存,后续逻辑出错。
修复方案
1. 更新状态类,添加刷新状态标记
修改SplashScreenState,增加追踪Token刷新状态的字段:
data class SplashScreenState( val isRefreshTokenExist: Boolean = false, val refreshToken: String? = null, val error: Boolean = false, val isRefreshingToken: Boolean = false, // 标记是否正在刷新Token val isTokenRefreshCompleted: Boolean = false // 标记刷新是否完成 )
2. 重构ViewModel的Token刷新逻辑
去掉runBlocking,改用viewModelScope处理异步任务,刷新开始和完成时更新状态,让Activity能感知:
@HiltViewModel class SplashScreenViewModel @Inject constructor( private val tokenManager: TokenManager ) : ViewModel() { private val _state = MutableStateFlow(SplashScreenState()) val state: StateFlow<SplashScreenState> = _state.asStateFlow() init { getRefreshToken() } fun refreshAccessToken(refreshToken: String) { viewModelScope.launch(Dispatchers.IO) { // 标记开始刷新 _state.update { it.copy(isRefreshingToken = true) } try { // 注意:Retrofit实例建议全局单例,不要每次刷新都创建,这里先保留原有逻辑 val loggingInterceptor = HttpLoggingInterceptor() loggingInterceptor.level = HttpLoggingInterceptor.Level.BODY val okHttpClient = OkHttpClient .Builder() .addInterceptor(loggingInterceptor) .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) } } // 标记刷新完成 _state.update { it.copy( isRefreshingToken = false, isTokenRefreshCompleted = true ) } } catch (e: Exception) { _state.update { it.copy( error = true, isRefreshingToken = false, isTokenRefreshCompleted = true ) } } } } private fun getRefreshToken() { viewModelScope.launch(Dispatchers.IO) { tokenManager.getRefreshToken().collect { refreshToken -> _state.update { it.copy( isRefreshTokenExist = refreshToken != null, refreshToken = refreshToken ) } } } } }
3. 修改SplashActivity的导航逻辑,等待刷新完成再跳转
只有当不需要刷新(无refreshToken)或者刷新完成后,才执行跳转逻辑:
@AndroidEntryPoint @SuppressLint("CustomSplashScreen") class SplashActivity : ComponentActivity() { private val viewModel: SplashScreenViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val splashScreen = installSplashScreen() splashScreen.setKeepOnScreenCondition { // 保持启动页显示直到刷新完成或无需刷新 viewModel.state.value.isRefreshingToken || (!viewModel.state.value.isTokenRefreshCompleted && viewModel.state.value.isRefreshTokenExist) } } super.onCreate(savedInstanceState) lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.state.collect { state -> val errMsg = "error occurs" if (state.error) { Toast.makeText(this@SplashActivity, errMsg, Toast.LENGTH_LONG).show() } // 两种情况跳转:1. 无refreshToken;2. 有refreshToken且已完成刷新 val shouldNavigate = !state.isRefreshTokenExist || (state.isRefreshTokenExist && state.isTokenRefreshCompleted) if (shouldNavigate) { val intent = Intent(this@SplashActivity, MainActivity::class.java) intent.putExtra("isTokenExist", state.isRefreshTokenExist) startActivity(intent) finish() } else if (state.isRefreshTokenExist && !state.isRefreshingToken) { // 存在refreshToken且未开始刷新时,触发刷新 state.refreshToken?.let { viewModel.refreshAccessToken(it) } } } } } } }
4. 优化建议:避免用Intent传值,直接在MainActivity中检查Token状态
去掉Intent传值,让MainActivity直接从TokenManager获取状态,减少耦合:
// MainActivity.kt @AndroidEntryPoint class MainActivity : ComponentActivity() { lateinit var navHostController: NavHostController @Inject lateinit var tokenManager: TokenManager // 注入TokenManager override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // 直接从TokenManager检查有效Token val isTokenExist = runBlocking { tokenManager.getAccessToken().firstOrNull() != null } // 屏幕尺寸、Google登出等原有代码... setContent { DiyetkolikTheme { navHostController = rememberNavController() Surface(modifier = Modifier.fillMaxSize()) { RootNavGraph(navHostController = navHostController, isTokenExist) } } } } // onActivityResult等原有代码... }
额外注意点
- Retrofit和OkHttpClient实例建议全局单例化,不要每次刷新Token都重新创建,避免资源浪费和性能损耗。
- 启动页的
keepOnScreenCondition结合状态动态控制,可避免不必要的固定延迟。
内容的提问来源于stack exchange,提问作者NewPartizal
相关产品推荐
相关产品推荐

