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

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"
)

问题根源

  1. DatePickerUI中chosenYear/chosenMonth/chosenDay仅以当前日期初始化,未读取ViewModel中已保存的日期值
  2. DateSelectionSection传递给InfiniteItemsPicker的firstIndex固定基于当前日期计算,未根据保存的日期动态调整
  3. 在Composition中直接读取StateFlow的.value(被@SuppressLint("StateFlowValueCalledInComposition")屏蔽警告),易引发状态不一致导致崩溃
  4. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 08:19:51