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

如何基于MVVM、UseCase、Hilt监听日期/时区变更更新UI

问题解决:时区/日期变更时自动更新Home数据与UI

核心问题分析

  1. TimeZoneBroadcastMonitor未实现currentDate Flow,无法监听系统日期变化
  2. ViewModel中selectedDate仅在初始化时计算,未响应时区/日期变更自动更新
  3. uiState组合逻辑未关联selectedDate的动态变化,导致UseCase不会重新执行

步骤1:完善TimeZoneBroadcastMonitor,实现日期监听

补充currentDate Flow,监听系统日期、时间变化的广播:

@Singleton
class TimeZoneBroadcastMonitor @Inject constructor(
    @ApplicationContext private val context: Context,
    @ApplicationScope appScope: CoroutineScope,
    @Dispatcher(IO) private val ioDispatcher: CoroutineDispatcher,
) : TimeZoneMonitor {

    // 原currentTimeZone实现保持不变...

    override val currentDate: SharedFlow<LocalDateTime> = callbackFlow {
        // 发送初始日期
        sendCurrentLocalDateTime()

        val receiver = object : BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                when (intent.action) {
                    Intent.ACTION_TIME_CHANGED,
                    Intent.ACTION_DATE_CHANGED,
                    Intent.ACTION_TIMEZONE_CHANGED -> sendCurrentLocalDateTime()
                }
            }

            private fun sendCurrentLocalDateTime() {
                val currentDateTime = LocalDateTime.now(TimeZone.currentSystemDefault())
                trySend(currentDateTime)
            }
        }

        val filter = IntentFilter().apply {
            addAction(Intent.ACTION_TIME_CHANGED)
            addAction(Intent.ACTION_DATE_CHANGED)
            addAction(Intent.ACTION_TIMEZONE_CHANGED)
        }
        context.registerReceiver(receiver, filter)

        awaitClose {
            context.unregisterReceiver(receiver)
        }
    }
        .distinctUntilChanged()
        .conflate()
        .flowOn(ioDispatcher)
        .shareIn(appScope, SharingStarted.WhileSubscribed(5_000), 1)
}

步骤2:重构HomeViewModel,响应时区/日期变化更新selectedDate

移除初始化时静态计算的日期逻辑,改为监听currentDate Flow自动更新selectedDate,同时保留SavedStateHandle的持久化:

@HiltViewModel
class HomeViewModel @Inject constructor(
    savedStateHandle: SavedStateHandle,
    private val analyticsHelper: AnalyticsHelper,
    timeZoneMonitor: TimeZoneMonitor,
    val userDataRepository: UserDataRepository,
    private val getHomeUseCase: GetHomeUseCase,
) : ViewModel() {

    private val selectedTopicIdKey = "selectedTopicIdKey"
    private val route: UniversalisRoute = savedStateHandle.toRoute()

    private val selectedTopicId = savedStateHandle.getStateFlow(
        key = selectedTopicIdKey,
        initialValue = route.initialTopicId.toString(),
    )

    // 监听时区变化
    val currentTimeZone = timeZoneMonitor.currentTimeZone
        .stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(5_000),
            TimeZone.currentSystemDefault(),
        )

    // 监听日期变化,并同步到SavedStateHandle
    private val currentDate = timeZoneMonitor.currentDate
        .stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(5_000),
            LocalDateTime.now(TimeZone.currentSystemDefault())
        )

    // 自动更新的selectedDate:由currentDate驱动,同时持久化到SavedStateHandle
    private val selectedDate = combine(
        currentDate,
        savedStateHandle.getStateFlow("date", 0)
    ) { date, savedDate ->
        val newDate = date.format(DateTimeFormatter.ofPattern("yyyyMMdd")).toInt()
        // 如果是初始值(0)或者日期变化,更新SavedStateHandle
        if (savedDate == 0 || newDate != savedDate) {
            savedStateHandle["date"] = newDate
        }
        newDate
    }
        .stateIn(
            viewModelScope,
            SharingStarted.WhileSubscribed(5_000),
            LocalDateTime.now(TimeZone.currentSystemDefault())
                .format(DateTimeFormatter.ofPattern("yyyyMMdd"))
                .toInt()
        )

    // 重构uiState:使用flatMapLatest,当selectedDate变化时重新执行UseCase
    val uiState: StateFlow<HomeUiState> = selectedDate
        .flatMapLatest { date ->
            combine(
                selectedTopicId,
                getHomeUseCase.invoke(date),
            ) { topicId, homeResource ->
                HomeUiState.HomeData(
                    selectedTopicId = topicId,
                    selectedDate = date,
                    topics = homeResource
                )
            }
        }
        .catch { throwable ->
            val error = HomeUiState.HomeError(
                date = selectedDate.value,
                message = throwable.message ?: "加载失败"
            )
            analyticsHelper.logHomeErrorEvent(error)
            emit(error)
        }
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.WhileSubscribed(5_000),
            initialValue = HomeUiState.Loading,
        )
}

关键修改点说明

  • 用currentDate Flow驱动selectedDate,确保时区/日期变化时自动更新日期值
  • 使用flatMapLatest替代原combine:当selectedDate变化时,会自动取消之前的UseCase调用并执行新的查询
  • 保留SavedStateHandle的持久化逻辑,确保配置变更后日期状态不丢失

步骤3:优化UI层状态接收(可选)

原HomeScreen已经接收currentTimeZone和currentDate,无需额外修改,但可以确保在Compose中正确观察State:

@ExperimentalLayoutApi
@Composable
fun HomeItems(
    uiState: HomeUiState,
    onTopicClick: (String) -> Unit,
    modifier: Modifier,
    haveDate: Boolean = false,
    currentTimeZone: State<TimeZone>,
    currentDate: State<LocalDateTime>
) {
    if (uiState is HomeUiState.HomeData) {
        Text(
            text = uiState.topics.data.fecha,
            modifier = Modifier.padding(2.dp),
            textAlign = TextAlign.Center,
        )
        // 可选:显示当前系统日期用于调试
        Text(
            text = "当前系统日期:${currentDate.value.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))}",
            modifier = Modifier.padding(4.dp)
        )
    }
}

额外注意事项

  • 确保isDateValid()函数能正确校验转换后的日期格式
  • getHomeUseCase中的universalisRepository.getUniversalisForTest(date).first()如果是阻塞调用,建议改为Flow形式,避免阻塞协程
  • 测试时可以通过系统设置手动修改时区/日期,验证数据是否自动更新

内容的提问来源于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.14 02:25:53