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) }
关键修改点总结
- ViewModel:使用
combine+flatMapLatest监听两个条件的变化,确保每次日期或按钮类型改变时触发新的请求。 - UI层:移除本地维护的
selectedDate、showTopics等状态,直接依赖ViewModel的状态,避免状态不一致。 - 状态过滤:在请求前判断日期和按钮类型的有效性,避免无效请求触发错误。
内容的提问来源于stack exchange,提问作者A. Cedano
相关产品推荐
相关产品推荐

