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

Jetpack Compose中如何基于双值通过ViewModel UIState更新UI

解决双条件(日期+按钮类型)触发数据请求的问题

核心问题分析

当前代码的关键问题在于ViewModel中未正确监听日期和按钮类型的双向变化,直接使用selectedDate.value和selectedTopicId.value调用UseCase只会获取初始值,无法在用户选择日期或点击按钮时触发新的请求。


1. ViewModel 修改:正确组合Flow触发请求

修改uiState的构建逻辑,通过combine监听selectedTopicId和selectedDate的变化,并在变化时调用UseCase获取数据:

@HiltViewModel
class CalendarViewModel @Inject constructor(
    private val savedStateHandle: SavedStateHandle,
    val userDataRepository: UserDataRepository,
    val universalisRepository: UniversalisRepository,
    private val getTopicWithDate: GetUniversalisFromCalendarUseCase,
) : ViewModel() {
    private val selectedTopicIdKey = "selectedTopicIdKey"
    private val selectedDateKey = "selectedDateKey"
    private val calendarRoute: CalendarRoute = savedStateHandle.toRoute()
    
    private val selectedTopicId = savedStateHandle.getStateFlow(
        key = selectedTopicIdKey,
        initialValue = "1"
    )
    
    private val selectedDate = savedStateHandle.getStateFlow(
        key = selectedDateKey,
        initialValue = 0 // 用0标记未选择日期
    )
    
    // 正确组合两个StateFlow,触发数据请求
    val uiState: StateFlow<CalendarUiState> = combine(selectedTopicId, selectedDate) { topicId, date ->
        Pair(topicId, date)
    }.flatMapLatest { (topicId, date) ->
        // 过滤无效值,避免初始状态触发请求
        if (date == 0 || topicId.isBlank()) {
            flowOf(CalendarUiState.Empty)
        } else {
            getTopicWithDate.invoke(
                date = date,
                title = "TODO:Title",
                selectedTopicId = topicId
            )
                .map { topics ->
                    if (topics.isEmpty()) CalendarUiState.Empty 
                    else CalendarUiState.CalendarData(topics)
                }
                .catch { emit(CalendarUiState.Error) } // 捕获请求异常
        }
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = CalendarUiState.Loading
    )

    fun onTopicClick(topicId: String?) {
        savedStateHandle[selectedTopicIdKey] = topicId ?: ""
    }

    fun onDateSelected(date: Long?) {
        savedStateHandle[selectedDateKey] = date ?: 0
    }
}

2. UI 层优化:状态联动与本地状态简化

2.1 修改CalendarScreen:传递ViewModel状态到子组件

移除冗余的onDateSelected参数,直接从ViewModel收集状态:

@Composable
fun CalendarScreen(
    onTopicClick: (String) -> Unit,
    viewModel: CalendarViewModel = hiltViewModel()
) {
    val uiState by viewModel.uiState.collectAsStateWithLifecycle()
    val selectedDate by viewModel.selectedDate.collectAsStateWithLifecycle()
    val selectedTopicId by viewModel.selectedTopicId.collectAsStateWithLifecycle()

    DatePickerAndButtons(
        onTopicClick = { topicId ->
            viewModel.onTopicClick(topicId)
            onTopicClick(topicId) // 如需外部导航,保留此调用
        },
        onDateSelected = viewModel::onDateSelected,
        uiState = uiState,
        selectedDate = selectedDate,
        selectedTopicId = selectedTopicId
    )
}

2.2 修改DatePickerAndButtons:移除本地冗余状态

基于ViewModel的状态控制UI显示,避免本地状态与ViewModel状态不一致:

@ExperimentalLayoutApi
@Composable
fun DatePickerAndButtons(
    onTopicClick: (String) -> Unit,
    onDateSelected: (Long?) -> Unit,
    uiState: CalendarUiState,
    selectedDate: Long,
    selectedTopicId: String
) {
    // 根据ViewModel状态控制弹窗和按钮显示
    val showDatePicker = remember(selectedDate) { selectedDate == 0 }
    val showTopicButtons = remember(selectedDate, uiState) { 
        selectedDate != 0 && uiState !is CalendarUiState.Loading 
    }

    // 状态UI展示
    when (uiState) {
        CalendarUiState.Empty -> Text("暂无数据")
        CalendarUiState.Error -> Text("加载出错,请重试")
        CalendarUiState.Loading -> CircularProgressIndicator()
        is CalendarUiState.CalendarData -> {
            if (uiState.topics.isNotEmpty()) {
                // 展示查询到的数据列表示例
                LazyColumn(modifier = Modifier.padding(16.dp)) {
                    items(uiState.topics) { topic ->
                        Button(
                            onClick = { onTopicClick(topic.id) },
                            modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)
                        ) {
                            Text(topic.name)
                        }
                    }
                }
            } else {
                Text("暂无匹配数据")
            }
        }
    }

    // 选中日期展示
    if (selectedDate != 0) {
        val date = Date(selectedDate)
        val formattedDate = SimpleDateFormat("MMM dd, yyyy", Locale.getDefault()).format(date)
        Text(
            "选中日期: $formattedDate",
            modifier = Modifier.padding(16.dp)
        )
    } else {
        Text("请选择日期", modifier = Modifier.padding(16.dp))
    }

    // 日期选择弹窗
    if (showDatePicker) {
        DatePickerModal(
            onDateSelected = { date ->
                onDateSelected(date)
            },
            onDismiss = { /* 可添加提示:必须选择日期才能继续 */ }
        )
    }

    // 按钮组展示(选择不同类型)
    if (showTopicButtons && (uiState is CalendarUiState.Empty || uiState is CalendarUiState.CalendarData && uiState.topics.isEmpty())) {
        Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
            Button(onClick = { onTopicClick("type1") }) {
                Text("类型1")
            }
            Button(onClick = { onTopicClick("type2") }) {
                Text("类型2")
            }
            Button(onClick = { onTopicClick("type3") }) {
                Text("类型3")
            }
        }
    }
}

2.3 修改NavGraphBuilder:移除冗余参数

composable<CalendarRoute> {
    CalendarScreen(onTopicClick = onTopicClick)
}

关键修改点总结

  1. ViewModel:使用combine+flatMapLatest监听两个条件的变化,确保每次日期或按钮类型改变时触发新的请求。
  2. UI层:移除本地维护的selectedDate、showTopics等状态,直接依赖ViewModel的状态,避免状态不一致。
  3. 状态过滤:在请求前判断日期和按钮类型的有效性,避免无效请求触发错误。

内容的提问来源于stack exchange,提问作者A. Cedano

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.17 23:44:55