fix(flutter): cacheFirst no longer hangs on no-match cold fetches

Liked tab loaded into an infinite spinner when the user had likes
in one category but not all three. Root cause: the three liked-tab
providers share one _populateLikeIds function. When the populate
writes track rows, drift watch fires for cached_likes (the table
all three providers watch). The track provider's stream re-emits
with rows.isNotEmpty → yields populated. The album and artist
streams re-emit with rows.isEmpty (user has no album/artist likes),
re-enter cacheFirst's rows-empty branch, fire populate AGAIN, drift
fires again, repeat — never yielding, .isLoading stays true forever,
UI spins.

Generalises beyond the liked case: any cacheFirst with a populate
that writes to a watched table but produces no rows matching this
filter would loop. Fix tracks coldFetchAttempted per subscription
so the first fetch is the only fetch via the rows-empty branch;
subsequent empty emissions yield empty. Also yields current rows
after a successful populate so a true no-op fetchAndPopulate (server
genuinely empty, fresh-install with no library data) doesn't hang
when drift doesn't re-emit for an empty batch.

For populated cases, the order is: spinner → brief empty yield from
the post-populate yield → drift watch re-emits with rows → populated.
UI flashes empty for one frame. Acceptable trade-off for the
no-spin guarantee.

Also matches the timeout pattern: liked providers' isOnline gains
the same 3-second timeout the home/library-list providers already
had, so a stuck connectivity check can't extend the hang.
This commit is contained in:
2026-05-14 15:21:55 -04:00
parent 28b0107925
commit 5511f87b4b
2 changed files with 38 additions and 5 deletions
@@ -242,7 +242,9 @@ final _likedTrackIdsProvider = StreamProvider<List<String>>((ref) {
driftStream: query,
fetchAndPopulate: () => _populateLikeIds(ref),
toResult: _idsForEntity,
isOnline: () async => (await ref.read(connectivityProvider.future)),
isOnline: () async => (await ref
.read(connectivityProvider.future)
.timeout(const Duration(seconds: 3), onTimeout: () => true)),
alwaysRefresh: true,
tag: 'likedTrackIds',
);
@@ -258,7 +260,9 @@ final _likedAlbumIdsProvider = StreamProvider<List<String>>((ref) {
driftStream: query,
fetchAndPopulate: () => _populateLikeIds(ref),
toResult: _idsForEntity,
isOnline: () async => (await ref.read(connectivityProvider.future)),
isOnline: () async => (await ref
.read(connectivityProvider.future)
.timeout(const Duration(seconds: 3), onTimeout: () => true)),
alwaysRefresh: true,
tag: 'likedAlbumIds',
);
@@ -274,7 +278,9 @@ final _likedArtistIdsProvider = StreamProvider<List<String>>((ref) {
driftStream: query,
fetchAndPopulate: () => _populateLikeIds(ref),
toResult: _idsForEntity,
isOnline: () async => (await ref.read(connectivityProvider.future)),
isOnline: () async => (await ref
.read(connectivityProvider.future)
.timeout(const Duration(seconds: 3), onTimeout: () => true)),
alwaysRefresh: true,
tag: 'likedArtistIds',
);