Jetpack Compose+Compose Destinations:登录流程页面数据保留问题
问题出在哪
你用remember { LoginPageData() }创建ViewModel的方式完全错了——remember只能在Composable的重组周期内保留对象,跳去验证码页面后,登录页的Composable会被销毁,返回时会重新执行remember生成新的LoginPageData实例,之前输的内容自然就没了。而且两个页面各用各的ViewModel,本来就没法共享登录流程的状态。
方案一:用导航图级别的共享ViewModel(最推荐)
Compose Destinations支持把一组页面归到同一个导航图里,ViewModel可以在整个导航图的作用域内共享,这样登录流程里的数据就不会丢了。
1. 给登录相关页面指定同一个导航图
先定义一个登录导航图的注解:
@NavGraph @RootNavGraph(start = true) annotation class LoginNavGraph( val start: Boolean = false )
然后给两个页面的@Destination注解加上这个导航图:
@Destination<LoginNavGraph> @Composable fun LoginLayout(){ // ... } @Destination<LoginNavGraph> @Composable fun CodeInputLayout(){ // ... }
2. 做一个统一的登录流程ViewModel
把登录需要的所有数据和逻辑都放到这个ViewModel里:
class LoginFlowViewModel : ViewModel() { // 存邮箱/手机号 val userInput = mutableStateOf("") // 存验证码 val verificationCode = mutableStateOf("") // 其他登录相关的逻辑,比如请求验证码、提交登录都放这 }
3. 两个页面都用这个共享ViewModel
别再各自创建独立ViewModel了,直接用viewModel()函数获取导航图级别的实例,两个页面拿到的是同一个:
修改LoginPage.kt:
@Composable fun LoginLayout(navigator: DestinationsNavigator){ val viewModel = viewModel<LoginFlowViewModel>() Column(){ TextField( value = viewModel.userInput.value, onValueChange = { viewModel.userInput.value = it }, label = { Text("邮箱/手机号") } ) Button( onClick ={ navigator.navigate(CodeInputPageDestination()) } ) { Text("获取验证码") } } }
修改CodeInputPage.kt:
@Composable fun CodeInputLayout(navigator: DestinationsNavigator){ val viewModel = viewModel<LoginFlowViewModel>() Column(){ TextField( value = viewModel.verificationCode.value, onValueChange = { viewModel.verificationCode.value = it }, label = { Text("验证码") } ) Button(onClick = { // 这里直接用viewModel里存的userInput和验证码做登录请求就行 navigator.popBackStack() }) { Text("返回") } } }
方案二:用SavedStateHandle恢复单个ViewModel的数据
如果不想用共享ViewModel,也可以给每个ViewModel加SavedStateHandle,这样就算ViewModel重建,数据也能从SavedState里捞回来:
修改LoginPageData.kt:
class LoginPageData(private val savedStateHandle: SavedStateHandle) : ViewModel(){ // 用savedStateHandle存数据,自动处理保存和恢复 val userInput = savedStateHandle.getStateFlow("user_input", "") .collectAsStateWithLifecycle() fun updateUserInput(input: String) { savedStateHandle["user_input"] = input } }
然后LoginLayout里换成viewModel()创建(别再用remember了):
@Composable fun LoginLayout(){ val viewModel = viewModel<LoginPageData>() Column(){ TextField( value = viewModel.userInput.value, onValueChange = { viewModel.updateUserInput(it) }, label = { Text("邮箱/手机号") } ) Button( onClick ={ navigator.navigate(CodeInputPageDestination()) } ) { Text("获取验证码") } } }
这种方法能让登录页的ViewModel重建时恢复之前的输入,但缺点是两个ViewModel没法直接共享数据,如果验证码页面要拿邮箱/手机号,还得通过导航参数传,不如方案一省心。
重点提醒
- 绝对别用
remember创建ViewModel,要用viewModel()或者hiltViewModel()——它们是由ViewModelStore管理的,能在页面生命周期内保留实例。 - 跨页面的流程状态,优先用导航图级别的共享ViewModel,数据统一管理,不用来回传参数。
内容的提问来源于stack exchange,提问作者郑迎风

