fix(flutter): album/artist detail covers + cacheFirst tracing for hangs
Album detail screen rendered the cover slot as a bare slate Container — no actual image. Wire ServerImage at /api/albums/<id>/cover (96dp, rounded). Same for artist detail: ClipOval around ServerImage of the artist's coverUrl, falling back to slate when empty. Artist navigation hang: artistProvider goes through cacheFirst, which had no logging — so we couldn't see whether the cold-cache fetch was running. Add an optional `tag` parameter that, when set, debugPrints each step (drift hit/miss, connectivity, fetch start/done/error). Wire tags into artistProvider and artistAlbumsProvider, plus add the 3s connectivity timeout there too as belt-and-suspenders. Next test pass should show cacheFirst[artist(<id>)]: lines that pinpoint where the artist screen is sticking.
This commit is contained in:
+13
-1
@@ -17,6 +17,8 @@
|
|||||||
|
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart' show debugPrint;
|
||||||
|
|
||||||
/// Wraps the watch + cold-cache fallback pattern. Generic over:
|
/// Wraps the watch + cold-cache fallback pattern. Generic over:
|
||||||
/// D — the drift row type (or TypedResult for joins)
|
/// D — the drift row type (or TypedResult for joins)
|
||||||
/// T — the result type the caller wants (e.g. `List<ArtistRef>`)
|
/// T — the result type the caller wants (e.g. `List<ArtistRef>`)
|
||||||
@@ -35,14 +37,19 @@ Stream<T> cacheFirst<D, T>({
|
|||||||
required T Function(List<D>) toResult,
|
required T Function(List<D>) toResult,
|
||||||
required Future<bool> Function() isOnline,
|
required Future<bool> Function() isOnline,
|
||||||
bool alwaysRefresh = false,
|
bool alwaysRefresh = false,
|
||||||
|
String? tag,
|
||||||
}) async* {
|
}) async* {
|
||||||
// Tracks whether we've already kicked off a stale-while-revalidate
|
// Tracks whether we've already kicked off a stale-while-revalidate
|
||||||
// refresh for this stream subscription, so we don't fire one on every
|
// refresh for this stream subscription, so we don't fire one on every
|
||||||
// drift re-emission (otherwise the populate cycles forever).
|
// drift re-emission (otherwise the populate cycles forever).
|
||||||
var revalidated = false;
|
var revalidated = false;
|
||||||
|
void log(String msg) {
|
||||||
|
if (tag != null) debugPrint('cacheFirst[$tag]: $msg');
|
||||||
|
}
|
||||||
|
|
||||||
await for (final rows in driftStream) {
|
await for (final rows in driftStream) {
|
||||||
if (rows.isNotEmpty) {
|
if (rows.isNotEmpty) {
|
||||||
|
log('drift hit (${rows.length} rows)');
|
||||||
yield toResult(rows);
|
yield toResult(rows);
|
||||||
// Stale-while-revalidate: yield cache immediately, then kick off
|
// Stale-while-revalidate: yield cache immediately, then kick off
|
||||||
// a REST refresh in the background. Drift watch() picks up the
|
// a REST refresh in the background. Drift watch() picks up the
|
||||||
@@ -55,15 +62,20 @@ Stream<T> cacheFirst<D, T>({
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
log('drift miss; checking connectivity');
|
||||||
if (await isOnline()) {
|
if (await isOnline()) {
|
||||||
|
log('online; calling fetchAndPopulate');
|
||||||
try {
|
try {
|
||||||
await fetchAndPopulate();
|
await fetchAndPopulate();
|
||||||
|
log('fetchAndPopulate done; awaiting drift re-emit');
|
||||||
// The drift watch() stream re-emits with the populated rows on
|
// The drift watch() stream re-emits with the populated rows on
|
||||||
// the next loop iteration. Don't yield here.
|
// the next loop iteration. Don't yield here.
|
||||||
} catch (_) {
|
} catch (e, st) {
|
||||||
|
log('fetchAndPopulate failed: $e\n$st');
|
||||||
yield toResult(rows); // empty result; caller surfaces error
|
yield toResult(rows); // empty result; caller surfaces error
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
log('offline; yielding empty');
|
||||||
yield toResult(rows); // empty result; offline
|
yield toResult(rows); // empty result; offline
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '../cache/audio_cache_manager.dart';
|
|||||||
import '../cache/db.dart';
|
import '../cache/db.dart';
|
||||||
import '../likes/like_button.dart';
|
import '../likes/like_button.dart';
|
||||||
import '../player/player_provider.dart';
|
import '../player/player_provider.dart';
|
||||||
|
import '../shared/widgets/server_image.dart';
|
||||||
import '../theme/theme_extension.dart';
|
import '../theme/theme_extension.dart';
|
||||||
import 'library_providers.dart';
|
import 'library_providers.dart';
|
||||||
import 'widgets/track_row.dart';
|
import 'widgets/track_row.dart';
|
||||||
@@ -27,7 +28,18 @@ class AlbumDetailScreen extends ConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
Container(width: 96, height: 96, color: fs.slate),
|
ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(6),
|
||||||
|
child: SizedBox(
|
||||||
|
width: 96,
|
||||||
|
height: 96,
|
||||||
|
child: ServerImage(
|
||||||
|
url: '/api/albums/$id/cover',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fallback: Container(color: fs.slate),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(child: Column(
|
Expanded(child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '../api/endpoints/likes.dart';
|
|||||||
import '../likes/like_button.dart';
|
import '../likes/like_button.dart';
|
||||||
import '../models/album.dart';
|
import '../models/album.dart';
|
||||||
import '../player/player_provider.dart';
|
import '../player/player_provider.dart';
|
||||||
|
import '../shared/widgets/server_image.dart';
|
||||||
import '../theme/theme_extension.dart';
|
import '../theme/theme_extension.dart';
|
||||||
import 'library_providers.dart';
|
import 'library_providers.dart';
|
||||||
import 'widgets/album_card.dart';
|
import 'widgets/album_card.dart';
|
||||||
@@ -30,9 +31,18 @@ class ArtistDetailScreen extends ConsumerWidget {
|
|||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Row(children: [
|
child: Row(children: [
|
||||||
Container(
|
ClipOval(
|
||||||
width: 96, height: 96,
|
child: SizedBox(
|
||||||
decoration: BoxDecoration(color: fs.slate, shape: BoxShape.circle),
|
width: 96,
|
||||||
|
height: 96,
|
||||||
|
child: a.coverUrl.isEmpty
|
||||||
|
? Container(color: fs.slate)
|
||||||
|
: ServerImage(
|
||||||
|
url: a.coverUrl,
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
fallback: Container(color: fs.slate),
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 16),
|
const SizedBox(width: 16),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|||||||
@@ -61,8 +61,10 @@ final artistProvider =
|
|||||||
toResult: (rows) => rows.isEmpty
|
toResult: (rows) => rows.isEmpty
|
||||||
? const ArtistRef(id: '', name: '')
|
? const ArtistRef(id: '', name: '')
|
||||||
: rows.first.toRef(),
|
: rows.first.toRef(),
|
||||||
isOnline: () async =>
|
isOnline: () async => (await ref
|
||||||
(await ref.read(connectivityProvider.future)),
|
.read(connectivityProvider.future)
|
||||||
|
.timeout(const Duration(seconds: 3), onTimeout: () => true)),
|
||||||
|
tag: 'artist($id)',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,8 +92,10 @@ final artistAlbumsProvider =
|
|||||||
final artist = r.readTableOrNull(db.cachedArtists);
|
final artist = r.readTableOrNull(db.cachedArtists);
|
||||||
return album.toRef(artistName: artist?.name ?? '');
|
return album.toRef(artistName: artist?.name ?? '');
|
||||||
}).toList(),
|
}).toList(),
|
||||||
isOnline: () async =>
|
isOnline: () async => (await ref
|
||||||
(await ref.read(connectivityProvider.future)),
|
.read(connectivityProvider.future)
|
||||||
|
.timeout(const Duration(seconds: 3), onTimeout: () => true)),
|
||||||
|
tag: 'artistAlbums($artistId)',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user