Skip to content

Commit 48ffc5e

Browse files
committed
perf: lazy-load lyrics only when lyrics view is visible
- Remove eager lyrics prefetch on every track change to avoid unnecessary network calls - Add ensureLyricsLoaded() method with lifecycle-aware guard (skip when app is paused/hidden) - Trigger lyrics fetch from full-screen player only when visible and app is resumed - Deduplicate prefetch calls using track key to prevent redundant requests
1 parent 4601c40 commit 48ffc5e

File tree

2 files changed

+56
-6
lines changed

2 files changed

+56
-6
lines changed

lib/providers/playback_provider.dart

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1209,8 +1209,6 @@ class PlaybackController extends Notifier<PlaybackState> {
12091209

12101210
if (!preserveQueue) {
12111211
_clearLyricsForTrackChange(upcomingItem: resolvingItem);
1212-
// Start lyrics lookup immediately while stream URL is still resolving.
1213-
unawaited(_fetchLyricsForItem(resolvingItem));
12141212
}
12151213
state = state.copyWith(
12161214
currentItem: resolvingItem,
@@ -1462,8 +1460,6 @@ class PlaybackController extends Notifier<PlaybackState> {
14621460
);
14631461

14641462
_clearLyricsForTrackChange(upcomingItem: item);
1465-
// Start lyrics lookup immediately while local source is preparing.
1466-
unawaited(_fetchLyricsForItem(item));
14671463

14681464
// Replacing stream playback with local playback should also replace queue,
14691465
// otherwise the old streaming queue remains visible in queue UI.
@@ -1761,8 +1757,6 @@ class PlaybackController extends Notifier<PlaybackState> {
17611757
clearError: true,
17621758
);
17631759
await _savePlaybackSnapshot();
1764-
// Start lyrics lookup at track-change time, not after playback starts.
1765-
unawaited(_fetchLyricsForItem(item));
17661760

17671761
// If the item has a Track but no resolved sourceUri, resolve stream
17681762
if (item.sourceUri.isEmpty && item.track != null) {
@@ -1990,8 +1984,23 @@ class PlaybackController extends Notifier<PlaybackState> {
19901984

19911985
/// Public method to manually refetch lyrics (e.g. retry button).
19921986
Future<void> refetchLyrics() async {
1987+
await ensureLyricsLoaded(force: true);
1988+
}
1989+
1990+
/// Load lyrics only when needed (e.g. when lyrics page is visible).
1991+
Future<void> ensureLyricsLoaded({bool force = false}) async {
19931992
final item = state.currentItem;
19941993
if (item == null) return;
1994+
final lifecycleState = WidgetsBinding.instance.lifecycleState;
1995+
if (!force &&
1996+
lifecycleState != null &&
1997+
lifecycleState != AppLifecycleState.resumed) {
1998+
return;
1999+
}
2000+
if (!force) {
2001+
if (state.lyricsLoading) return;
2002+
if (state.lyrics != null) return;
2003+
}
19952004
await _fetchLyricsForItem(item);
19962005
}
19972006

lib/widgets/mini_player_bar.dart

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,19 +201,58 @@ class _FullScreenPlayerState extends ConsumerState<_FullScreenPlayer> {
201201
late final PageController _pageController;
202202
bool _isScrubbing = false;
203203
double _scrubSeconds = 0;
204+
String? _lastLyricsPrefetchKey;
205+
AppLifecycleListener? _appLifecycleListener;
206+
bool _isAppResumed = true;
204207

205208
@override
206209
void initState() {
207210
super.initState();
208211
_pageController = PageController();
212+
final initialState = WidgetsBinding.instance.lifecycleState;
213+
_isAppResumed =
214+
initialState == null || initialState == AppLifecycleState.resumed;
215+
_appLifecycleListener = AppLifecycleListener(
216+
onResume: () {
217+
_isAppResumed = true;
218+
if (!mounted) return;
219+
final state = ref.read(playbackProvider);
220+
_prefetchLyricsForCurrentTrack(state);
221+
},
222+
onPause: () => _isAppResumed = false,
223+
onHide: () => _isAppResumed = false,
224+
onDetach: () => _isAppResumed = false,
225+
onInactive: () => _isAppResumed = false,
226+
);
209227
}
210228

211229
@override
212230
void dispose() {
231+
_appLifecycleListener?.dispose();
232+
_appLifecycleListener = null;
213233
_pageController.dispose();
214234
super.dispose();
215235
}
216236

237+
String _lyricsPrefetchKey(PlaybackItem item) {
238+
return '${item.id}|${item.title}|${item.artist}';
239+
}
240+
241+
void _prefetchLyricsForCurrentTrack(PlaybackState state) {
242+
if (!_isAppResumed) return;
243+
final item = state.currentItem;
244+
if (item == null) return;
245+
246+
final key = _lyricsPrefetchKey(item);
247+
if (_lastLyricsPrefetchKey == key) return;
248+
_lastLyricsPrefetchKey = key;
249+
250+
WidgetsBinding.instance.addPostFrameCallback((_) {
251+
if (!mounted) return;
252+
unawaited(ref.read(playbackProvider.notifier).ensureLyricsLoaded());
253+
});
254+
}
255+
217256
void _switchToLyrics() {
218257
setState(() => _currentPage = 1);
219258
_pageController.animateToPage(
@@ -246,12 +285,14 @@ class _FullScreenPlayerState extends ConsumerState<_FullScreenPlayer> {
246285
final playbackError = _localizedPlaybackError(context, state);
247286
final item = state.currentItem;
248287
if (item == null) {
288+
_lastLyricsPrefetchKey = null;
249289
// Track stopped, close the player
250290
WidgetsBinding.instance.addPostFrameCallback((_) {
251291
if (mounted) Navigator.of(context).pop();
252292
});
253293
return const SizedBox.shrink();
254294
}
295+
_prefetchLyricsForCurrentTrack(state);
255296

256297
final colorScheme = Theme.of(context).colorScheme;
257298
final textTheme = Theme.of(context).textTheme;

0 commit comments

Comments
 (0)