You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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,提问作者郑迎风

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.07 12:10:37