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

如何强制更新MutableStateFlow?解决日期变更时UI不更新问题

问题描述

我正在开发每日挑战页面,当应用处于打开状态时日期发生变更(例如从2022年11月7日23:59到11月8日00:00),页面状态无法随之更新。我的实现代码如下:

data class MyObject(val startDate: Date, val lastVote: List<Date> = emptyList()) {
    fun isChallengeDone(): Boolean {
        return Calendar.getInstance().time >= startDate
    }
}

@HiltViewModel
class DailyChallengeViewModel: ViewModel() {

    private val _currentDay = MutableStateFlow(Calendar.getInstance())
        
    private val _myObject = MutableStateFlow(MyObject(
            Calendar.getInstance().apply {
                set(Calendar.DATE, 8)
                set(Calendar.HOUR_OF_DAY, 0)
                set(Calendar.MINUTE , 0)
                set(Calendar.SECOND , 0)
                set(Calendar.MILLISECOND , 0)
            }.time
        ))
    val myObject = _myObject.asStateFlow()

    init {
        _myObject.combine(_currentDay) { obj, _ ->
            _myObject.value = obj
        }.launchIn(viewModelScope)
    }

    fun updateDate() {
        _currentDay.value = Calendar.getInstance()
    }
}

@Composable
fun DailyChallengeScreen(viewModel: DailyChallengeViewModel = hiltViewModel()) {
    val context = LocalContext.current
    DisposableEffect(context) {
        val receiver = object: BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                viewModel.updateDate()
            }
        }
        context.registerReceiver(receiver, IntentFilter(Intent.ACTION_DATE_CHANGED))
        onDispose {
            context.unregisterReceiver(receiver)
        }
    }
    val myObject by viewModel.myObject.collectAsStateWithLifecycle()
    DailyChallengeContent(myObject)
}

@Composable
fun DailyChallengeContent(myObject: MyObject) {
    Column(verticalArrangement = Arrangement.spacedBy(20.dp)) {
        Text("${Calendar.getInstance().time} >= ${myObject.startDate}")
        Text(myObject.isChallengeDone().toString())
    }
}

我知道_myObject.combine(_currentDay) { obj, _ -> _myObject.value = obj }无法触发更新,因为_myObject.value与obj原子相等,请问如何实现日期变更时更新对应的Composable?


解决方案

方案1:将挑战完成状态转为派生状态

isChallengeDone的结果依赖实时时间,没必要绑定在MyObject实例中,直接在ViewModel里通过currentDay和myObject组合出派生状态即可:

修改ViewModel

@HiltViewModel
class DailyChallengeViewModel: ViewModel() {

    private val _currentDay = MutableStateFlow(Calendar.getInstance())
        
    private val _myObject = MutableStateFlow(MyObject(
            Calendar.getInstance().apply {
                set(Calendar.DATE, 8)
                set(Calendar.HOUR_OF_DAY, 0)
                set(Calendar.MINUTE , 0)
                set(Calendar.SECOND , 0)
                set(Calendar.MILLISECOND , 0)
            }.time
        ))
    val myObject = _myObject.asStateFlow()

    // 派生状态:根据当前时间和myObject计算是否完成挑战
    val isChallengeDone = combine(myObject, _currentDay) { obj, _ ->
        obj.isChallengeDone()
    }.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = _myObject.value.isChallengeDone()
    )

    fun updateDate() {
        _currentDay.value = Calendar.getInstance()
    }
}

修改Composable

@Composable
fun DailyChallengeScreen(viewModel: DailyChallengeViewModel = hiltViewModel()) {
    val context = LocalContext.current
    DisposableEffect(context) {
        val receiver = object: BroadcastReceiver() {
            override fun onReceive(context: Context, intent: Intent) {
                viewModel.updateDate()
            }
        }
        context.registerReceiver(receiver, IntentFilter(Intent.ACTION_DATE_CHANGED))
        onDispose {
            context.unregisterReceiver(receiver)
        }
    }
    val myObject by viewModel.myObject.collectAsStateWithLifecycle()
    val isDone by viewModel.isChallengeDone.collectAsStateWithLifecycle()
    DailyChallengeContent(myObject, isDone)
}

@Composable
fun DailyChallengeContent(myObject: MyObject, isChallengeDone: Boolean) {
    Column(verticalArrangement = Arrangement.spacedBy(20.dp)) {
        Text("${Calendar.getInstance().time} >= ${myObject.startDate}")
        Text(isChallengeDone.toString())
    }
}

方案2:强制更新MyObject实例

如果要保留MyObject中的isChallengeDone方法,可以在日期变更时,通过data class的copy()方法创建新实例,触发StateFlow的更新:

修改ViewModel

@HiltViewModel
class DailyChallengeViewModel: ViewModel() {

    private val _currentDay = MutableStateFlow(Calendar.getInstance())
        
    private val _myObject = MutableStateFlow(MyObject(
            Calendar.getInstance().apply {
                set(Calendar.DATE, 8)
                set(Calendar.HOUR_OF_DAY, 0)
                set(Calendar.MINUTE , 0)
                set(Calendar.SECOND , 0)
                set(Calendar.MILLISECOND , 0)
            }.time
        ))
    val myObject = _myObject.asStateFlow()

    init {
        // 监听日期变更,通过copy创建新实例触发更新
        _currentDay.onEach {
            _myObject.value = _myObject.value.copy()
        }.launchIn(viewModelScope)
    }

    fun updateDate() {
        _currentDay.value = Calendar.getInstance()
    }
}

这样日期变更时,_myObject会发射新的实例,Composable会自动收集状态并刷新。


补充建议

  • 优先使用java.time包下的API(如LocalDate、Instant)替代Calendar和Date,它们线程安全且API设计更简洁。
  • ACTION_DATE_CHANGED广播无法覆盖所有时间变更场景(比如用户手动修改系统时间),如果需要更全面的监听,可以额外注册Intent.ACTION_TIME_CHANGED广播。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.13 20:15:39