From c8109d9197975b0758c3e209e609b71b591bc1c7 Mon Sep 17 00:00:00 2001 From: Jonas Haugesen Date: Fri, 11 Sep 2026 21:01:03 +0200 Subject: [PATCH] Keep clock widget parts alive across lifecycle stops `partProviders` was a `stateIn(WhileSubscribed)` flow, so its upstream map ran again every time the clock widget was subscribed after the launcher had been in the background - and built new part providers. Providers cache what their ranking is based on, so the new agenda part started at ranking 0, vanished while the launcher was gone and only came back once its query had run again. Build the provider list in `viewModelScope` into a `MutableStateFlow` instead, so the instances (and their cached state) survive for as long as the ViewModel does. Their ranking flows are still only collected while the widget is subscribed, so observers are still registered and unregistered with the widget's lifecycle. --- .../launcher/widgets/clock/ClockWidgetVM.kt | 42 ++++++++++++------- 1 file changed, 28 insertions(+), 14 deletions(-) diff --git a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt index 1c58158b4..74217ea85 100644 --- a/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt +++ b/app/ui/src/main/java/de/mm20/launcher2/ui/launcher/widgets/clock/ClockWidgetVM.kt @@ -17,33 +17,47 @@ import de.mm20.launcher2.ui.launcher.widgets.clock.parts.MusicPartProvider import de.mm20.launcher2.ui.launcher.widgets.clock.parts.PartProvider import de.mm20.launcher2.ui.launcher.widgets.clock.parts.SmartspacerPartProvider import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.channelFlow import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import org.koin.core.component.KoinComponent import org.koin.core.component.inject class ClockWidgetVM : ViewModel(), KoinComponent { private val settings: ClockWidgetSettings by inject() - private val partProviders = settings.parts.combine(settings.useSmartspacer) { p, s -> - p to s - }.map { (parts, smartspacer) -> - if (smartspacer && isAtLeastApiLevel(29)) { - return@map listOf(SmartspacerPartProvider()) - } + /** + * Kept for the lifetime of the ViewModel instead of being rebuilt whenever the clock widget + * is subscribed again - which happens on every return to the home screen, because the + * launcher stops while it is in the background. Providers cache the data behind their + * ranking, so a fresh instance starts at ranking 0 and makes its part disappear until its + * query returns; the agenda part would blink on every visit that way. + */ + private val partProviders = MutableStateFlow>(emptyList()) - val providers = mutableListOf() - if (parts.date) providers += DatePartProvider() - if (parts.calendar) providers += CalendarPartProvider() - if (parts.music) providers += MusicPartProvider() - providers += BatteryPartProvider(parts.battery) - if (parts.alarm) providers += AlarmPartProvider() - providers - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(), emptyList()) + init { + viewModelScope.launch { + settings.parts.combine(settings.useSmartspacer) { p, s -> p to s } + .collect { (parts, smartspacer) -> + partProviders.value = if (smartspacer && isAtLeastApiLevel(29)) { + listOf(SmartspacerPartProvider()) + } else { + buildList { + if (parts.date) add(DatePartProvider()) + if (parts.calendar) add(CalendarPartProvider()) + if (parts.music) add(MusicPartProvider()) + add(BatteryPartProvider(parts.battery)) + if (parts.alarm) add(AlarmPartProvider()) + } + } + } + } + } fun getActiveParts(context: Context): Flow> = channelFlow { partProviders.collectLatest { providers ->