fix(widget): refresh from setter, serialized with a mutex
All checks were successful
Translations / check (pull_request) Successful in 4s
CI / ci (pull_request) Successful in 4m29s

The debounced observer was unreliable: drop(1) plus the ViewModel lifecycle
could swallow a change after re-entering the app, so a single range change
sometimes didn't refresh the widget. Refresh directly from the settings
setters instead (always fires on a real change), serialized through a mutex
so a rapid flip-and-flip-back can't run two updateAll calls at once and
strand the widget on the intermediate value.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 16:44:52 +02:00
parent cd703e9d54
commit bb92d02000

View File

@@ -19,25 +19,18 @@ import de.jeanlucmakiola.calendula.ui.agenda.AgendaRange
import de.jeanlucmakiola.calendula.ui.common.CalendarView import de.jeanlucmakiola.calendula.ui.common.CalendarView
import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidget import de.jeanlucmakiola.calendula.widget.agenda.AgendaWidget
import de.jeanlucmakiola.calendula.widget.month.MonthWidget import de.jeanlucmakiola.calendula.widget.month.MonthWidget
import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import javax.inject.Inject import javax.inject.Inject
private const val WIDGET_REFRESH_DEBOUNCE_MS = 350L
@OptIn(FlowPreview::class)
@HiltViewModel @HiltViewModel
class SettingsViewModel @Inject constructor( class SettingsViewModel @Inject constructor(
private val prefs: SettingsPrefs, private val prefs: SettingsPrefs,
@@ -123,30 +116,11 @@ class SettingsViewModel @Inject constructor(
initialValue = SettingsUiState(dynamicColorAvailable = dynamicColorAvailable), initialValue = SettingsUiState(dynamicColorAvailable = dynamicColorAvailable),
) )
init { // Serialises widget redraws so a rapid flip-and-flip-back can't run two
// Push widget redraws when their backing prefs change, debounced so a // updateAll calls at once — concurrent calls coalesce around a stale read
// rapid flip-and-flip-back settles to one update of the final value // and can leave the widget on the intermediate value. Held sequentially,
// (two racing updateAll calls can otherwise leave the widget stale). // the last call reads the final committed pref and renders it.
// drop(1) skips the value present at startup — nothing to refresh yet. private val widgetRefreshMutex = Mutex()
prefs.agendaWidgetRange
.drop(1)
.debounce(WIDGET_REFRESH_DEBOUNCE_MS)
.distinctUntilChanged()
.onEach { AgendaWidget().updateAll(appContext) }
.launchIn(viewModelScope)
// Both widgets depend on the week start (month weekday header; the
// agenda widget's "this week" range).
prefs.weekStart
.drop(1)
.debounce(WIDGET_REFRESH_DEBOUNCE_MS)
.distinctUntilChanged()
.onEach {
AgendaWidget().updateAll(appContext)
MonthWidget().updateAll(appContext)
}
.launchIn(viewModelScope)
}
private data class ReminderDefaults( private data class ReminderDefaults(
val allowColor: Boolean, val allowColor: Boolean,
@@ -179,7 +153,15 @@ class SettingsViewModel @Inject constructor(
} }
fun setWeekStart(pref: WeekStartPref) { fun setWeekStart(pref: WeekStartPref) {
viewModelScope.launch { prefs.setWeekStart(pref) } viewModelScope.launch {
prefs.setWeekStart(pref)
// Both widgets depend on the week start (month weekday header; the
// agenda widget's "this week" range).
widgetRefreshMutex.withLock {
AgendaWidget().updateAll(appContext)
MonthWidget().updateAll(appContext)
}
}
} }
fun setAgendaScreenRange(range: AgendaRange) { fun setAgendaScreenRange(range: AgendaRange) {
@@ -187,7 +169,10 @@ class SettingsViewModel @Inject constructor(
} }
fun setAgendaWidgetRange(range: AgendaRange) { fun setAgendaWidgetRange(range: AgendaRange) {
viewModelScope.launch { prefs.setAgendaWidgetRange(range) } viewModelScope.launch {
prefs.setAgendaWidgetRange(range)
widgetRefreshMutex.withLock { AgendaWidget().updateAll(appContext) }
}
} }
fun setAgendaShowRangeBar(enabled: Boolean) { fun setAgendaShowRangeBar(enabled: Boolean) {