feat(flutter): SWR everywhere + nav hydration + cold-start home skeleton
Three changes addressing the cold-start spinner + stale-on-revisit pain. A. SWR on remaining cacheFirst providers - artistProvider, artistAlbumsProvider, artistTracksProvider all gain alwaysRefresh: true. Cache hit still renders instantly; one background refresh per subscription keeps the row from going stale forever. - albumProvider (inline async*) and playlistDetailProvider (inline async*) now keep a `revalidated` flag and kick a one-shot background fetch on the first complete cache hit. Same effect as cacheFirst's alwaysRefresh, just inline. - Three providers gained the connectivity timeout that album/artist/playlist already had. B. Navigation hydration - AlbumDetailScreen accepts `AlbumRef? seed`; ArtistDetailScreen accepts `ArtistRef? seed`. When the live provider is still loading, the seed populates cover/title/artist immediately so the page isn't blank. - Routing wires `extra: AlbumRef|ArtistRef` from go_router into the seed parameter. - Call sites updated: home (Recently added, Rediscover albums + artists, Last played), artist detail album grid. Where a ref isn't available (track actions sheet), the screen falls back to the spinner — no regression. C. Cold-start home skeleton - Replace the full-screen CircularProgressIndicator on /home with a layout-preserving skeleton: 5 section titles + 6 grey card-shaped placeholders per row. The page feels populated immediately; sections fill in independently as data arrives via the per-section providers. - Drops the unused DelayedLoading import. Net effect: re-visits to detail screens render instantly (cache hit + silent refresh); first visit from a tile shows the seed header immediately while tracks load; cold-start home shows a layout skeleton instead of a 30s blank spinner.
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:drift/drift.dart' as drift;
|
||||
import 'package:flutter/foundation.dart' show debugPrint;
|
||||
@@ -64,6 +66,9 @@ final artistProvider =
|
||||
isOnline: () async => (await ref
|
||||
.read(connectivityProvider.future)
|
||||
.timeout(const Duration(seconds: 3), onTimeout: () => true)),
|
||||
// SWR: yield cache instantly on hit, refresh in the background so
|
||||
// the row catches up to server state without making the user wait.
|
||||
alwaysRefresh: true,
|
||||
tag: 'artist($id)',
|
||||
);
|
||||
});
|
||||
@@ -95,6 +100,7 @@ final artistAlbumsProvider =
|
||||
isOnline: () async => (await ref
|
||||
.read(connectivityProvider.future)
|
||||
.timeout(const Duration(seconds: 3), onTimeout: () => true)),
|
||||
alwaysRefresh: true,
|
||||
tag: 'artistAlbums($artistId)',
|
||||
);
|
||||
});
|
||||
@@ -128,8 +134,11 @@ final artistTracksProvider =
|
||||
albumTitle: album?.title ?? '',
|
||||
);
|
||||
}).toList(),
|
||||
isOnline: () async =>
|
||||
(await ref.read(connectivityProvider.future)),
|
||||
isOnline: () async => (await ref
|
||||
.read(connectivityProvider.future)
|
||||
.timeout(const Duration(seconds: 3), onTimeout: () => true)),
|
||||
alwaysRefresh: true,
|
||||
tag: 'artistTracks($artistId)',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -160,6 +169,10 @@ final albumProvider = StreamProvider.family<
|
||||
// Once-per-subscription guard so we don't re-fetch in a loop if the
|
||||
// server genuinely returns zero tracks (or if the fetch fails).
|
||||
var fetchAttempted = false;
|
||||
// SWR revalidation guard — fire one background refresh per
|
||||
// subscription on the first complete cache hit, so the displayed
|
||||
// data catches up to server state without making the user wait.
|
||||
var revalidated = false;
|
||||
|
||||
Future<bool> isOnline() async {
|
||||
try {
|
||||
@@ -290,6 +303,16 @@ final albumProvider = StreamProvider.family<
|
||||
);
|
||||
}).toList();
|
||||
|
||||
// SWR: first complete cache hit triggers one background refresh
|
||||
// so we don't show stale data indefinitely. Drift watch re-emits
|
||||
// on success and the next iteration yields the fresh data.
|
||||
if (!revalidated && trackRows.isNotEmpty) {
|
||||
revalidated = true;
|
||||
if (await isOnline()) {
|
||||
unawaited(fetchAndPopulate());
|
||||
}
|
||||
}
|
||||
|
||||
yield (album: album, tracks: tracks);
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user