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.
This commit is contained in:
2026-09-11 21:01:03 +02:00
parent 3b3d6473ca
commit c8109d9197

View File

@@ -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<List<PartProvider>>(emptyList())
val providers = mutableListOf<PartProvider>()
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<List<PartProvider>> = channelFlow {
partProviders.collectLatest { providers ->