Jetpack Compose中如何用共享ViewModel共享TextField手机号
问题解答
完全可以实现,基于Hilt + Jetpack Compose导航的共享ViewModel机制就能做到,改动量很小。
实现思路
你现在的核心问题是手机号输入状态存在RegisterScreen内部的remember变量里,页面跳转后该状态会随Composition销毁,两个独立业务的ViewModel拿不到同一份输入值。单独抽离一个共享ViewModel存储跨页面的手机号状态,让注册页、验证码页持有同一个SharedViewModel实例即可解决。
具体实现步骤
- 首先定义共享ViewModel
@HiltViewModel class SharedViewModel @Inject constructor() : ViewModel() { private val _phoneNumber = MutableStateFlow("") val phoneNumber: StateFlow<String> = _phoneNumber.asStateFlow() fun updatePhoneNumber(number: String) { // 可在此处提前增加手机号格式校验逻辑 _phoneNumber.value = number } }
注意不要把这个ViewModel绑定到Application全局作用域,否则退出注册流程后数据会残留,绑定到注册流程对应的导航图作用域即可,流程退出时ViewModel会自动回收。
- 修改注册页代码,把手机号状态从页面内部移到共享ViewModel
把原来页面内remember的手机号状态替换成从SharedViewModel收集的状态,输入变化时直接更新到共享VM:
@OptIn(ExperimentalComposeUiApi::class) @Composable fun RegisterScreen( navController: NavController, model: RegisterViewModel, sharedVm: SharedViewModel = hiltViewModel() // 注入共享VM实例 ) { val focusManager = LocalFocusManager.current // 收集共享VM里的手机号状态 val phoneNumber by sharedVm.phoneNumber.collectAsStateWithLifecycle() OutlinedTextField( value = phoneNumber, colors = TextFieldDefaults.textFieldColors( backgroundColor = white, focusedIndicatorColor = Grey, unfocusedIndicatorColor = Grey, focusedLabelColor = Grey, unfocusedLabelColor = Grey, cursorColor = custom, textColor = custom, ), onValueChange = { sharedVm.updatePhoneNumber(it) }, // 输入更新直接同步到共享VM label = { Text(text = "Phone Number") }, keyboardActions = KeyboardActions(onNext = { focusManager.moveFocus(FocusDirection.Down) }), placeholder = { Text(text = "Phone Number") }, singleLine = true, modifier = Modifier.fillMaxWidth(0.8f) ) Button( modifier = Modifier .width(205.dp) .height(35.dp), onClick = { focus.clearFocus(force = true) model.onSignUp(phoneNumber) // 直接取共享VM的手机号传给写入Firestore的逻辑 navController.navigate("otp") }, colors = ButtonDefaults.buttonColors( backgroundColor = custom ), shape = RoundedCornerShape(30) ) { Text( text = "Next", style = TextStyle( fontSize = 11.sp, color = white, )) } }
- 修改验证码页代码,从同一个SharedViewModel实例读取之前输入的手机号
验证码页注入同一个SharedViewModel,就能直接拿到用户在注册页输入的手机号,不需要通过导航参数传递:
@OptIn(ExperimentalComposeUiApi::class) @Composable fun PhoneVerifyScreen( navController: NavController, modelAuthentication: AuthenticationViewModel, onClick: (mobileNum: String, otp: String) -> Unit, sharedVm: SharedViewModel = hiltViewModel() // 同一个导航栈下拿到的是和注册页相同的实例 ) { val focusManager = LocalFocusManager.current val phoneNumberOTP = remember { mutableStateOf("") } // 读取共享VM里存的手机号 val phoneNumber by sharedVm.phoneNumber.collectAsStateWithLifecycle() val context = LocalContext.current LaunchedEffect(Unit) { println("found activity? ${context.findActivity()}") val activity = context.findActivity() ?: return@LaunchedEffect modelAuthentication.setActivity(activity) // 可在此处直接触发发送验证码逻辑,不用等用户额外操作 // modelAuthentication.sendVerificationCode(phoneNumber) } Column( Modifier.fillMaxSize() .clickable( indication = null, interactionSource = remember { MutableInteractionSource() } ) {keyboardController?.hide()}, verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally ) { OutlinedTextField( value = phoneNumberOTP.value, colors = TextFieldDefaults.textFieldColors( backgroundColor = white, focusedIndicatorColor = Grey, unfocusedIndicatorColor = Grey, focusedLabelColor = Grey, unfocusedLabelColor = Grey, cursorColor = custom, textColor = custom, ), onValueChange = { phoneNumberOTP.value = it }, label = { Text(text = "Verify code") }, placeholder = { Text(text = "Verify code") }, singleLine = true, modifier = Modifier.fillMaxWidth(0.8f), ) CompositionLocalProvider(LocalRippleTheme provides NoRippleTheme) { Button( modifier = Modifier .width(285.dp) .height(55.dp), onClick = { // 把共享VM里的手机号和用户输入的验证码一起传给验证逻辑 modelAuthentication.otpVerification( phoneNumber = phoneNumber, otp = phoneNumberOTP.value ) navController.navigate("profileScreen") }, colors = ButtonDefaults.buttonColors( backgroundColor = custom2 ), shape = RoundedCornerShape(60), ) { Text( text = "Next", style = TextStyle( fontSize = 18.sp, color = white, )) } } } }
补充说明
- 如果给注册流程做了嵌套导航图,只需要在导航图的作用域内获取SharedViewModel,就能保证整个注册流程(注册页->验证码页->资料完善页)拿到的是同一个实例,退出注册流程后ViewModel会自动销毁,不会残留数据。
- 不需要修改现有
AuthViewModel和RegisterViewModel的构造逻辑,手机号直接在Composable层从共享VM取出后传给对应业务方法即可,业务VM之间完全解耦。 - 相比通过导航路由参数传递手机号,用共享VM的方式不需要处理参数序列化、长度限制的问题,后续如果要加其他跨页面的注册表单字段(比如昵称、密码),直接在SharedViewModel里加对应状态即可,拓展性更强。
内容的提问来源于stack exchange,提问作者SpaceDevs
相关产品推荐
相关产品推荐

