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

如何解决DatePicker Dialog无法返回日期更新ViewModel显示问题

问题根因
  • 类型不匹配:TradeDatePicker回调返回LocalDate类型,但事件类EnteredTradeDate定义参数为Long,传值直接失败
  • ViewModel滥用Compose API:remember是仅可在Composable作用域使用的状态持有方法,ViewModel中调用完全不会生效
  • 日期状态定义错误:原代码中tradeDate是函数类型,不是可观察状态,UI层无法收到更新;事件处理逻辑中调用了不存在的_tradeDate.value、tradeDate.value.copy方法,逻辑完全不通
  • 选择器未绑定状态:给TradeDatePicker传的value固定写死LocalDate.now(),选中后重组仍会重置为默认值,且组件内未展示选中日期文本
  • 回调触发时机错误:vanpra组件的onDateChange是滑动选日期时实时触发,不是点确认按钮才触发,会导致选择过程中状态乱跳

分步修复方案

1. 修正事件密封类

对齐回调参数类型,把EnteredTradeDate的参数改为LocalDate:

sealed class AddEditTradeEvent {
    data class EnteredSymbol(val value: String): AddEditTradeEvent()
    data class ChangeSymbolFocus(val focusState: FocusState): AddEditTradeEvent()
    data class EnteredQuantity(val value: String): AddEditTradeEvent()
    data class ChangeQuantityFocus(val focusState: FocusState): AddEditTradeEvent()
    // 修改参数类型为LocalDate
    data class EnteredTradeDate(val value: LocalDate) : AddEditTradeEvent()
    data class ChangeDateFocus(val focusState: FocusState): AddEditTradeEvent()
    object SaveTrade: AddEditTradeEvent()
}

2. 修正ViewModel状态逻辑

删除错误的remember调用和函数类型状态,用和其他字段一致的mutableStateOf持有可观察状态:

@HiltViewModel
class AddEditTradeViewModel @Inject constructor(
    private val tradeUseCases: TradeUseCases,
    savedStateHandle: SavedStateHandle
) : ViewModel(){
    private val _tradeSymbol = mutableStateOf(
        TradeTextFieldState(hint = "Enter Trade Symbol")
    )
    val tradeSymbol: State<TradeTextFieldState> = _tradeSymbol

    private val _tradeQuantity = mutableStateOf(
        TradeTextFieldState(hint = "Enter Number of Shares")
    )
    val tradeQuantity: State<TradeTextFieldState> = _tradeQuantity

    // 正确定义可观察的日期状态,默认值为当前日期
    private val _tradeDate = mutableStateOf(LocalDate.now())
    val tradeDate: State<LocalDate> = _tradeDate
    // 派生格式化后的展示文本,UI直接拿这个值渲染
    val tradeDateDisplay: String
        get() = _tradeDate.value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))

    private val _eventFlow = MutableSharedFlow<UiEvent>()
    val eventFlow = _eventFlow.asSharedFlow()
    private var currentTradeId: Int? = null

    init{
        savedStateHandle.get<Int>("tradeId")?.let { tradeId ->
            if (tradeId != -1){
                viewModelScope.launch {
                    tradeUseCases.getTrade(tradeId)?.also { trade ->
                        currentTradeId = trade.id
                        _tradeSymbol.value = _tradeSymbol.value.copy(
                            text = trade.tradeSymb,
                            isHintVisible = false
                        )
                        _tradeQuantity.value = _tradeQuantity.value.copy(
                            text = trade.tradequant,
                            isHintVisible = false
                        )
                        // 编辑回显时把存的时间戳转回LocalDate
                        _tradeDate.value = Instant.ofEpochMilli(trade.timestamp)
                            .atZone(ZoneId.systemDefault())
                            .toLocalDate()
                    }
                }
            }
        }
    }

    fun onEvent(event: AddEditTradeEvent){
        when(event){
            is AddEditTradeEvent.EnteredSymbol -> {
                _tradeSymbol.value = _tradeSymbol.value.copy(text = event.value)
            }
            is AddEditTradeEvent.ChangeSymbolFocus -> {
                _tradeSymbol.value = _tradeSymbol.value.copy(
                    isHintVisible = !event.focusState.isFocused && _tradeSymbol.value.text.isBlank()
                )
            }
            is AddEditTradeEvent.EnteredQuantity -> {
                _tradeQuantity.value = _tradeQuantity.value.copy(text = event.value)
            }
            is AddEditTradeEvent.ChangeQuantityFocus -> {
                _tradeQuantity.value = _tradeQuantity.value.copy(
                    isHintVisible = !event.focusState.isFocused && _tradeQuantity.value.text.isBlank()
                )
            }
            // 修正日期事件处理逻辑
            is AddEditTradeEvent.EnteredTradeDate -> {
                _tradeDate.value = event.value
            }
            is AddEditTradeEvent.SaveTrade -> {
                viewModelScope.launch{
                    try {
                        tradeUseCases.addTrade(
                            Trade(
                                tradeSymb = _tradeSymbol.value.text,
                                tradequant= _tradeQuantity.value.text,
                                // 存库时把LocalDate转成Long时间戳
                                timestamp = _tradeDate.value.atStartOfDay(ZoneId.systemDefault())
                                    .toInstant().toEpochMilli(),
                                id = currentTradeId
                            )
                        )
                        _eventFlow.emit(UiEvent.SaveTrade)
                    } catch (e: InvalidTradeException){
                        _eventFlow.emit(
                            UiEvent.ShowSnackbar(message = e.message ?: "Couldn't save trade")
                        )
                    }
                }
            }
        }
    }

    sealed class UiEvent {
        data class ShowSnackbar(val message: String): UiEvent()
        object SaveTrade: UiEvent()
    }
}

3. 修正TradeDatePicker组件

增加临时选中值,点确认按钮才回传状态,同时在组件内展示选中的日期:

@Composable
fun TradeDatePicker(
    value: LocalDate,
    onValueChanged: (LocalDate) -> Unit,
    modifier:Modifier = Modifier,
) {
    val dialogState = rememberMaterialDialogState()
    // 临时存弹窗内选中的日期,点OK才正式回传
    var tempSelectedDate by remember { mutableStateOf(value) }

    MaterialDialog(
        dialogState = dialogState,
        buttons = {
            positiveButton("OK") {
                onValueChanged(tempSelectedDate)
            }
            negativeButton("CANCEL")
        },
        backgroundColor = MaterialTheme.colors.background
    ) {
        datepicker(
            initialDate = value,
            onDateChange = { newDate ->
                tempSelectedDate = newDate
            }
        )
    }
    Box(
        modifier = modifier
            .border(
                width = 1.dp,
                color = MaterialTheme.colors.onPrimary,
                shape = RoundedCornerShape(50),
            )
            .clip(RoundedCornerShape(50)) // 换回你自己定义的ButtonShape即可
            .clickable {
                // 打开弹窗时重置临时值为当前已选中的日期
                tempSelectedDate = value
                dialogState.show()
            }
    ){
        Row (modifier = Modifier.padding(16.dp)){
            Text(
                // 展示选中的日期,不要写死文本
                text = value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")),
                modifier = Modifier.weight(1F),
            )
            Icon(
                Icons.Default.DateRange,
                contentDescription = "Select Date",
            )
        }
    }
}

4. 修正页面调用逻辑

把选择器的value绑定ViewModel持有的状态,不要写死默认值:

@Composable
fun AddEditTradeScreen(
    navController: NavController,
    viewModel: AddEditTradeViewModel = hiltViewModel()
){
    val tradesymbolState = viewModel.tradeSymbol.value
    val tradequantState = viewModel.tradeQuantity.value
    // 取ViewModel持有的日期状态
    val tradeDateState = viewModel.tradeDate.value
    val scaffoldState = rememberScaffoldState()

    LaunchedEffect(Unit){
        viewModel.eventFlow.collectLatest { event ->
            when(event){
                is AddEditTradeViewModel.UiEvent.ShowSnackbar ->{
                    scaffoldState.snackbarHostState.showSnackbar(message = event.message)
                }
                is AddEditTradeViewModel.UiEvent.SaveTrade -> {
                    navController.navigateUp()
                }
            }
        }
    }
    
    Scaffold(
        floatingActionButton = {
            FloatingActionButton(
                onClick = { viewModel.onEvent(AddEditTradeEvent.SaveTrade) },
                backgroundColor = MaterialTheme.colors.primary
            ) {
                Icon(imageVector = Icons.Default.Save, contentDescription = "Save Trade")
            }
        },
        scaffoldState = scaffoldState
    ) { padding ->
        Column(
            modifier = Modifier
                .fillMaxSize()
                .padding(16.dp)
                .padding(padding)
        ) {
            Text(
                modifier = Modifier.fillMaxWidth(),
                text = "Enter Your Trade",
                style = MaterialTheme.typography.h4,
                textAlign = TextAlign.Center
            )
            Spacer(modifier = Modifier.height(20.dp))

            TransparentHintTextField(
                text = tradesymbolState.text,
                hint = tradesymbolState.hint,
                onValueChange = { viewModel.onEvent(AddEditTradeEvent.EnteredSymbol(it)) },
                onFocusChange = { viewModel.onEvent(AddEditTradeEvent.ChangeSymbolFocus(it)) },
                isHintVisible = tradesymbolState.isHintVisible,
                singleLine = true,
                textStyle = MaterialTheme.typography.h5
            )
            Spacer(modifier = Modifier.height(16.dp))

            TransparentHintTextField(
                text = tradequantState.text,
                hint = tradequantState.hint,
                onValueChange = { viewModel.onEvent(AddEditTradeEvent.EnteredQuantity(it)) },
                onFocusChange = { viewModel.onEvent(AddEditTradeEvent.ChangeQuantityFocus(it)) },
                isHintVisible = tradequantState.isHintVisible,
                textStyle = MaterialTheme.typography.body1,
            )
            Spacer(modifier = Modifier.height(16.dp))

            // 绑定ViewModel状态,不要写死LocalDate.now()
            TradeDatePicker(
                value = tradeDateState,
                onValueChanged = { selectedDate ->
                    viewModel.onEvent(AddEditTradeEvent.EnteredTradeDate(selectedDate))
                },
                modifier = Modifier.fillMaxWidth()
            )
        }
    }
}

注意事项
  • 所有remember开头的Compose状态API只能在@Composable注解的函数内调用,ViewModel是生命周期独立于UI的普通类,禁止在此类中调用Compose专属API
  • 如果项目minSdk低于26,需要在模块build.gradle中开启核心库脱糖才能使用java.time包下的LocalDate、DateTimeFormatter等API
  • UI层统一用LocalDate做状态方便渲染,持久化存储时再转成Long类型时间戳,不要在UI层来回做类型转换增加出错概率

内容的提问来源于stack exchange,提问作者JustADude

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 08:42:14