From 48366186093857480943f969df63b2abf5234fb9 Mon Sep 17 00:00:00 2001 From: Jonas Haugesen Date: Fri, 11 Sep 2026 21:17:57 +0200 Subject: [PATCH] Stop calendar queries emitting an empty list before they have run `queryCalendarEvents` forwarded the `MutableStateFlow` it accumulates the provider results in, so the initial empty value was the first thing collectors saw - before any provider had reported. Callers in the launcher take that first result (`CalendarPartProvider` uses `.first()`) and cannot tell it apart from "no events", so the agenda stayed empty and its part never showed up. `CalendarRepository.findMany`'s 500 ms debounce used to hide this: the placeholder was superseded by the real list within the debounce window, which is exactly why the agenda only ever appeared half a second late. Keep the accumulator null until the first provider reports, and let `findMany` - a one-shot query for the clock and calendar widgets - wait for all providers (`awaitAllProviders`) so it never hands out a partial list. The search path keeps streaming results as providers report, now without an empty flash. --- .../launcher2/calendar/CalendarRepository.kt | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt b/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt index 9736872bf..ff12af9b3 100644 --- a/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt +++ b/data/calendar/src/main/java/de/mm20/launcher2/calendar/CalendarRepository.kt @@ -22,10 +22,12 @@ import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combineTransform import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.emitAll +import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transform import kotlinx.coroutines.flow.update +import kotlinx.coroutines.joinAll import kotlinx.coroutines.launch import kotlinx.coroutines.supervisorScope import kotlin.time.Duration.Companion.days @@ -126,6 +128,7 @@ internal class CalendarRepositoryImpl( excludeAllDayEvents = excludeAllDayEvents, excludeCalendars = excludeCalendars, providers = providers, + awaitAllProviders = true, allowNetwork = false, ) emitAll(if (debounceMillis > 0L) events.debounce(debounceMillis) else events) @@ -139,12 +142,17 @@ internal class CalendarRepositoryImpl( excludeAllDayEvents: Boolean = false, excludeCalendars: List = emptyList(), allowNetwork: Boolean = false, + awaitAllProviders: Boolean = false, providers: List, ): Flow> = flow { supervisorScope { - val result = MutableStateFlow(persistentListOf()) + // Null until the first provider has reported. Emitting the (empty) accumulator right + // away would hand collectors an empty list before any query had run, which callers + // that take the first result - like the agenda in the clock widget - cannot tell + // apart from "no events". + val result = MutableStateFlow?>(null) - for (provider in providers) { + val jobs = providers.map { provider -> launch { val r = provider.search( query, @@ -158,11 +166,19 @@ internal class CalendarRepositoryImpl( allowNetwork = allowNetwork, ) result.update { - (it + r).toPersistentList() + ((it ?: persistentListOf()) + r).toPersistentList() } } } - emitAll(result) + + if (awaitAllProviders) { + // One-shot callers must not receive a partial list, so wait until every + // provider has reported; if there is none, report an empty result. + jobs.joinAll() + emit(result.value ?: persistentListOf()) + } else { + emitAll(result.filterNotNull()) + } } }