Jetpack Compose日期选择器弹窗无法回显已选日期问题求助
Jetpack Compose日期选择器弹窗状态恢复问题
实现背景
基于Jetpack Compose实现了日期选择器弹窗,核心逻辑:
- 通过OutlinedTextField叠加透明按钮,保持和其他无选择器字段的样式统一
- 点击TextField后弹出带顶部标签的弹窗,内部包含三个无限滚动选择器,支持在指定范围选择年、月、日
- 点击「Done」按钮后关闭弹窗,并更新
GlobalViewModel中的对应日期字段
遇到的问题
首次选择日期后重新打开弹窗,不会显示之前选中的日期,默认展示当前日期。尝试添加条件判断,用ViewModel保存的日期替换当前日期作为初始值时,应用出现崩溃。
问题代码
@Composable fun DatePickerUI( label: String, globalViewModel: GlobalSavedViewModel, onDismissRequest: () -> Unit ) { Card( shape = RoundedCornerShape(10.dp), ) { Column( horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier .fillMaxWidth() .padding(vertical = 10.dp, horizontal = 5.dp) ) { Text( text = label, modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, style = AdditionalTypography.bodyMedium ) Spacer(modifier = Modifier.height(30.dp)) val chosenYear = remember { mutableStateOf(currentYear) } val chosenMonth = remember { mutableStateOf(currentMonth) } val chosenDay = remember { mutableStateOf(currentDay) } DateSelectionSection( onYearChosen = { chosenYear.value = it.toInt() }, onMonthChosen = { chosenMonth.value = monthsNames.indexOf(it) }, onDayChosen = { chosenDay.value = it.toInt() }, globalViewModel = globalViewModel, dateType = label, ) Spacer(modifier = Modifier.height(30.dp)) Button( shape = RoundedCornerShape(5.dp), modifier = Modifier .fillMaxWidth() .padding(horizontal = 10.dp), onClick = { val date = chosenYear.value.toString().substring(2) + if(chosenMonth.value.toString().length == 1) {"0"} else {""} + (chosenMonth.value + 1).toString() + if(chosenDay.value.toString().length == 1) {"0"} else {""} + chosenDay.value.toString() if(label == "Date of Birth") { globalViewModel.updateDateOfBirth(date) } else { globalViewModel.updateDateOfExpiration(date) } onDismissRequest() } ) { Text( text = "Done", modifier = Modifier.fillMaxWidth(), textAlign = TextAlign.Center, style = AdditionalTypography.bodyMedium ) } } } } @SuppressLint("StateFlowValueCalledInComposition") @Composable fun DateSelectionSection( onYearChosen: (String) -> Unit, onMonthChosen: (String) -> Unit, onDayChosen: (String) -> Unit, globalViewModel: GlobalSavedViewModel, dateType: String, ) { Row( horizontalArrangement = Arrangement.SpaceAround, modifier = Modifier .fillMaxWidth() .height(100.dp) ) { InfiniteItemsPicker( items = days, firstIndex = Int.MAX_VALUE / 2 + (currentDay - 2), onItemSelected = onDayChosen ) InfiniteItemsPicker( items = monthsNames, firstIndex = Int.MAX_VALUE / 2 - 4 + currentMonth, onItemSelected = onMonthChosen ) InfiniteItemsPicker( items = years, firstIndex = Int.MAX_VALUE / 2 + (currentYear - 1925), onItemSelected = onYearChosen ) } } @SuppressLint("FrequentlyChangedStateReadInComposition") @Composable fun InfiniteItemsPicker( items: List<String>, firstIndex: Int, onItemSelected: (String) -> Unit, ) { val listState = rememberLazyListState(firstIndex) val currentValue = remember { mutableStateOf("") } LaunchedEffect(key1 = !listState.isScrollInProgress) { onItemSelected(currentValue.value) listState.animateScrollToItem(index = listState.firstVisibleItemIndex) } Box(modifier = Modifier.height(106.dp)) { LazyColumn( horizontalAlignment = Alignment.CenterHorizontally, state = listState, content = { items(count = Int.MAX_VALUE, itemContent = { val index = it % items.size if (it == listState.firstVisibleItemIndex + 1) { currentValue.value = items[index] } Spacer(modifier = Modifier.height(6.dp)) Text( text = items[index], modifier = Modifier.alpha(if (it == listState.firstVisibleItemIndex + 1) 1f else 0.3f), textAlign = TextAlign.Center, style = AdditionalTypography.bodyMedium ) Spacer(modifier = Modifier.height(6.dp)) }) } ) } } @Composable fun formatMRZDate(date: String): String { if(date.length == 6) { Log.i("DatePicker", date.substring(2,4) + "-" + date.substring(4,6) + "-" + date.substring(0,2)) return date.substring(2,4) + "-" + date.substring(4,6) + "-" + date.substring(0,2) } return "" } val currentYear = Calendar.getInstance().get(Calendar.YEAR) val currentDay = Calendar.getInstance().get(Calendar.DAY_OF_MONTH) val currentMonth = Calendar.getInstance().get(Calendar.MONTH) val years = (1900..2100).map { it.toString() } val monthsNumber = (1..12).map { it.toString() } val days = (1..31).map { it.toString() } val monthsNames = listOf( "JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC" )
问题根源
DatePickerUI中chosenYear/chosenMonth/chosenDay仅以当前日期初始化,未读取ViewModel中已保存的日期值DateSelectionSection传递给InfiniteItemsPicker的firstIndex固定基于当前日期计算,未根据保存的日期动态调整- 在Composition中直接读取StateFlow的
.value(被@SuppressLint("StateFlowValueCalledInComposition")屏蔽警告),易引发状态不一致导致崩溃 InfiniteItemsPicker中频繁在Composition阶段读取listState.firstVisibleItemIndex,违反Compose状态读写规范
修复方案
1. 解析ViewModel保存的日期,初始化选择状态
修改DatePickerUI,先从ViewModel获取已保存的日期字符串,解析为年、月、日作为初始值:
@Composable fun DatePickerUI( label: String, globalViewModel: GlobalSavedViewModel, onDismissRequest: () -> Unit ) { // 正确观察ViewModel的StateFlow状态 val savedDate = when(label) { "Date of Birth" -> globalViewModel.dateOfBirth.collectAsState() else -> globalViewModel.dateOfExpiration.collectAsState() }.value // 解析保存的日期,无值则用当前日期 val (initYear, initMonth, initDay) = parseSavedDate(savedDate) ?: Triple(currentYear, currentMonth, currentDay) // 以savedDate为key,确保日期更新时状态重新初始化 val chosenYear = remember(savedDate) { mutableStateOf(initYear) } val chosenMonth = remember(savedDate) { mutableStateOf(initMonth) } val chosenDay = remember(savedDate) { mutableStateOf(initDay) } // 原Card、Column及Button逻辑保持不变... // 更新DateSelectionSection调用,传递初始值 DateSelectionSection( onYearChosen = { chosenYear.value = it.toInt() }, onMonthChosen = { chosenMonth.value = monthsNames.indexOf(it) }, onDayChosen = { chosenDay.value = it.toInt() }, initYear = chosenYear.value, initMonth = chosenMonth.value, initDay = chosenDay.value, ) } // 新增日期解析函数,适配YYMMDD格式 private fun parseSavedDate(dateStr: String?): Triple<Int, Int, Int>? { if (dateStr?.length != 6) return null val year = "20${dateStr.substring(0,2)}".toInt() val month = dateStr.substring(2,4).toInt() - 1 // 转换为Calendar的0-index月份 val day = dateStr.substring(4,6).toInt() return Triple(year, month, day) }
2. 动态计算选择器初始索引
修改DateSelectionSection,接收初始年、月、日,为每个选择器计算正确的初始滚动位置:
@Composable fun DateSelectionSection( onYearChosen: (String) -> Unit, onMonthChosen: (String) -> Unit, onDayChosen: (String) -> Unit, initYear: Int, initMonth: Int, initDay: Int, ) { Row( horizontalArrangement = Arrangement.SpaceAround, modifier = Modifier .fillMaxWidth() .height(100.dp) ) { // 计算日选择器初始索引:基于选中日在days列表中的位置 val dayFirstIndex = Int.MAX_VALUE / 2 + (initDay - 1) InfiniteItemsPicker( items = days, firstIndex = dayFirstIndex, onItemSelected = onDayChosen ) // 计算月选择器初始索引:基于选中月在monthsNames列表中的索引 val monthFirstIndex = Int.MAX_VALUE / 2 + initMonth InfiniteItemsPicker( items = monthsNames, firstIndex = monthFirstIndex, onItemSelected = onMonthChosen ) // 计算年选择器初始索引:基于选中年在years列表中的位置(years从1900开始) val yearFirstIndex = Int.MAX_VALUE / 2 + (initYear - 1900) InfiniteItemsPicker( items = years, firstIndex = yearFirstIndex, onItemSelected = onYearChosen ) } }
3. 修正InfiniteItemsPicker的状态逻辑
移除违规的状态读取,改用LaunchedEffect处理滚动状态监听:
@Composable fun InfiniteItemsPicker( items: List<String>, firstIndex: Int, onItemSelected: (String) -> Unit, ) { val listState = rememberLazyListState(firstIndex) // 初始化时设置默认选中值 val currentValue = remember { mutableStateOf(items[firstIndex % items.size]) } // 初始化回调一次选中值 LaunchedEffect(Unit) { onItemSelected(currentValue.value) } // 滚动停止时更新选中值并居中 LaunchedEffect(listState.isScrollInProgress) { if (!listState.isScrollInProgress) { val middleItemIndex = listState.firstVisibleItemIndex + 1 val itemIndex = middleItemIndex % items.size currentValue.value = items[itemIndex] onItemSelected(currentValue.value) listState.animateScrollToItem(middleItemIndex) } } Box(modifier = Modifier.height(106.dp)) { LazyColumn( horizontalAlignment = Alignment.CenterHorizontally, state = listState, content = { items(count = Int.MAX_VALUE) { index -> val itemIndex = index % items.size val isSelected = index == listState.firstVisibleItemIndex + 1 Spacer(modifier = Modifier.height(6.dp)) Text( text = items[itemIndex], modifier = Modifier.alpha(if (isSelected) 1f else 0.3f), textAlign = TextAlign.Center, style = AdditionalTypography.bodyMedium ) Spacer(modifier = Modifier.height(6.dp)) } } ) } }
4. 确保ViewModel使用StateFlow规范
验证GlobalSavedViewModel的日期字段用StateFlow定义,确保状态变化能正确通知Compose:
class GlobalSavedViewModel : ViewModel() { private val _dateOfBirth = MutableStateFlow("") val dateOfBirth = _dateOfBirth.asStateFlow() private val _dateOfExpiration = MutableStateFlow("") val dateOfExpiration = _dateOfExpiration.asStateFlow() fun updateDateOfBirth(date: String) { _dateOfBirth.value = date } fun updateDateOfExpiration(date: String) { _dateOfExpiration.value = date } }
关键说明
- 使用
collectAsState()观察StateFlow,避免直接读取.value导致的状态不一致 - 为
remember添加savedDate作为key,确保ViewModel日期更新时选择状态同步刷新 - 动态计算选择器初始索引,让弹窗打开时直接定位到之前选中的日期
- 遵循Compose状态读写规范,仅在
LaunchedEffect或回调中处理状态变更,避免Composition阶段的无效读写
内容的提问来源于stack exchange,提问作者nikoparas1
相关产品推荐
相关产品推荐

