屏幕旋转时保留ViewModel:Dagger注入致登录页状态丢失
你的问题核心在于直接通过Dagger注入ViewModel实例,没有利用ViewModel自身的生命周期管理机制——屏幕旋转时Activity重建,Dagger会提供全新的ViewModel实例,导致之前的状态和API调用状态全部丢失。下面是具体的解决方案,一步步帮你修复这个问题:
1. 先理清问题根源
ViewModel的设计初衷就是在配置变化(比如屏幕旋转)时保留状态,它的生命周期独立于Activity的重建。但如果直接用@Inject在Activity里注入ViewModel,Dagger会在每次Activity创建时都生成新实例,完全绕过了ViewModel的生命周期管理机制。
另外,你在onDestroy里取消API调用也不合理——屏幕旋转触发的onDestroy只是临时销毁Activity,ViewModel此时并不会被销毁,真正需要取消API调用的时机是ViewModel被彻底销毁(比如用户退出Activity)。
2. 调整ViewModel的实现与注入方式
我们需要让Dagger提供ViewModel.Factory,然后通过ViewModelProvider获取ViewModel,这样系统就能在配置变化时保留原ViewModel实例。
步骤1:改造LoginViewModel
用viewModelScope管理API调用的协程,它会在ViewModel真正销毁时自动取消所有协程,无需手动在Activity里处理:
class LoginViewModel @Inject constructor( private val loginRepository: LoginRepository ) : ViewModel() { // 用LiveData暴露登录状态 private val _loginState = MutableLiveData<LoginState>() val loginState: LiveData<LoginState> = _loginState fun triggerLogin(username: String, password: String) { // 避免重复触发加载 if (_loginState.value is LoginState.Loading) return viewModelScope.launch { _loginState.value = LoginState.Loading try { val loginResult = loginRepository.login(username, password) _loginState.value = LoginState.Success(loginResult) } catch (e: Exception) { _loginState.value = LoginState.Error(e.message ?: "登录失败") } } } // 密封类定义登录状态 sealed class LoginState { object Loading : LoginState() data class Success(val data: LoginResponse) : LoginState() data class Error(val message: String) : LoginState() } }
步骤2:创建ViewModelFactory
让Dagger注入这个Factory,它负责提供ViewModel实例:
class LoginViewModelFactory @Inject constructor( private val loginViewModel: Provider<LoginViewModel> ) : ViewModelProvider.Factory { @Suppress("UNCHECKED_CAST") override fun <T : ViewModel> create(modelClass: Class<T>): T { require(modelClass.isAssignableFrom(LoginViewModel::class.java)) { "Unknown ViewModel class: ${modelClass.name}" } return loginViewModel.get() as T } }
步骤3:修改LoginActivity的ViewModel获取逻辑
不再直接注入ViewModel,而是通过ViewModelProvider获取,确保配置变化时复用原实例:
class LoginActivity : AppCompatActivity() { @Inject lateinit var loginViewModelFactory: LoginViewModelFactory private lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) // 注入ViewModelFactory (application as YourApp).appComponent.inject(this) // 获取ViewModel——屏幕旋转时会复用原实例 loginViewModel = ViewModelProvider(this, loginViewModelFactory)[LoginViewModel::class.java] // 观察登录状态变化 loginViewModel.loginState.observe(this) { state -> when (state) { LoginViewModel.LoginState.Loading -> showLoading() is LoginViewModel.LoginState.Success -> handleLoginSuccess(state.data) is LoginViewModel.LoginState.Error -> showError(state.message) } } // 按钮点击触发登录 loginBtn.setOnClickListener { val username = usernameEt.text.toString().trim() val password = passwordEt.text.toString().trim() loginViewModel.triggerLogin(username, password) } } // 移除onDestroy里的API取消逻辑——viewModelScope会自动处理 // override fun onDestroy() { // super.onDestroy() // // 这里的取消逻辑可以删掉 // } }
步骤4:配置Dagger组件
确保你的AppComponent能正确注入LoginViewModelFactory:
@Singleton @Component(modules = [/* 你的Repository、网络模块等 */]) interface AppComponent { fun inject(activity: LoginActivity) fun loginViewModelFactory(): LoginViewModelFactory }
3. 为什么这样能解决问题?
- ViewModelProvider的作用:它会跟踪ViewModel的生命周期,配置变化时不会销毁原ViewModel,新的Activity会直接复用已有实例,状态(比如LiveData的加载状态、协程执行情况)完全保留。
- viewModelScope的自动管理:ViewModel的
onCleared()方法会在Activity真正被销毁(比如用户点击返回键退出)时调用,此时viewModelScope会自动取消所有未完成的协程,无需手动在Activity的onDestroy里处理。 - Dagger与ViewModel的配合:通过Factory注入,既保留了Dagger的依赖注入能力,又让ViewModel的生命周期被系统正确管理。
内容的提问来源于stack exchange,提问作者Sandip Fichadiya

