22152b1ba3
Across-the-board sluggishness was every cold-cache tap doing one network round trip while the user waits. SWR helps on re-visits but the first time you tap a tile from home you eat the latency. MetadataPrefetcher listens to homeProvider. When /api/home returns, it fires fire-and-forget reads on albumProvider + artistProvider for the top-N items in each home section (recently added, rediscover, most played, last played). Each provider read triggers the existing cold-cache path, which writes to drift. By the time the user actually taps a tile, it's already a drift hit and the detail screen renders instantly. Cap N=8 per section (covers what's visible on a typical phone without scrolling). Set spans dedupe across sections so popular artists don't get fetched five times. Errors are swallowed — a failed prefetch is silent and the tile falls back to its on-tap fetch behavior. Wired alongside the existing audio prefetcher in app.dart's postFrameCallback.
78 lines
2.8 KiB
Dart
78 lines
2.8 KiB
Dart
import 'package:flutter/foundation.dart' show debugPrint;
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
|
|
import '../library/library_providers.dart';
|
|
import '../models/home_data.dart';
|
|
|
|
/// Pre-warms the drift cache for entities the user is most likely to
|
|
/// tap. When /api/home returns its sections, fire one albumProvider /
|
|
/// artistProvider read per top-of-section item in the background. The
|
|
/// reads trigger the existing cold-cache path, which writes drift.
|
|
/// By the time the user actually taps a tile, it's a drift hit and
|
|
/// the detail screen renders instantly.
|
|
///
|
|
/// All reads are unawaited and idempotent — providers that already
|
|
/// have a cached value short-circuit. Readers/writers don't conflict
|
|
/// because each provider de-duplicates concurrent subscribers.
|
|
class MetadataPrefetcher {
|
|
MetadataPrefetcher(this._ref) {
|
|
_ref.listen<AsyncValue<HomeData>>(homeProvider, (_, next) {
|
|
next.whenData(_warmHome);
|
|
});
|
|
}
|
|
|
|
final Ref _ref;
|
|
|
|
/// Cap per section so a 50-album recently-added row doesn't fan
|
|
/// out 50 fetches the moment the screen appears. Top-N covers what
|
|
/// the user can see without scrolling on a typical phone.
|
|
static const _topN = 8;
|
|
|
|
void _warmHome(HomeData h) {
|
|
final albumIds = <String>{};
|
|
final artistIds = <String>{};
|
|
|
|
for (final a in h.recentlyAddedAlbums.take(_topN)) {
|
|
if (a.id.isNotEmpty) albumIds.add(a.id);
|
|
if (a.artistId.isNotEmpty) artistIds.add(a.artistId);
|
|
}
|
|
for (final a in h.rediscoverAlbums.take(_topN)) {
|
|
if (a.id.isNotEmpty) albumIds.add(a.id);
|
|
if (a.artistId.isNotEmpty) artistIds.add(a.artistId);
|
|
}
|
|
for (final ar in h.rediscoverArtists.take(_topN)) {
|
|
if (ar.id.isNotEmpty) artistIds.add(ar.id);
|
|
}
|
|
for (final ar in h.lastPlayedArtists.take(_topN)) {
|
|
if (ar.id.isNotEmpty) artistIds.add(ar.id);
|
|
}
|
|
for (final t in h.mostPlayedTracks.take(_topN)) {
|
|
if (t.albumId.isNotEmpty) albumIds.add(t.albumId);
|
|
if (t.artistId.isNotEmpty) artistIds.add(t.artistId);
|
|
}
|
|
|
|
debugPrint(
|
|
'metadataPrefetcher: warming ${albumIds.length} albums + ${artistIds.length} artists');
|
|
|
|
for (final id in albumIds) {
|
|
_swallow(_ref.read(albumProvider(id).future));
|
|
}
|
|
for (final id in artistIds) {
|
|
_swallow(_ref.read(artistProvider(id).future));
|
|
}
|
|
}
|
|
|
|
/// Discards the return value and any error from a fire-and-forget
|
|
/// provider read. We don't care about the value here — we only want
|
|
/// the side effect of writing drift.
|
|
void _swallow(Future<Object?> f) {
|
|
f.then<void>((_) {}).onError((_, __) {});
|
|
}
|
|
}
|
|
|
|
/// Read once at app start to activate the prefetcher (e.g. wire it
|
|
/// from a top-level Consumer or main.dart container override).
|
|
final metadataPrefetcherProvider = Provider<MetadataPrefetcher>((ref) {
|
|
return MetadataPrefetcher(ref);
|
|
});
|