mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-15 14:25:33 +02:00
perf(ui): retain track history lookup snapshots
Share the retained history lookup snapshot across albums, playlists, artists, home search results, and library folders. Reuse derived lookups until the track list is replaced. Test reuse with 4,000 items and invalidation after replacement, sorting, and filtering.
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/screens/track_history_snapshot.dart';
|
||||
import 'package:spotiflac_android/widgets/collection_scaffold.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
@@ -78,6 +79,7 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen>
|
||||
with
|
||||
SelectionModeMixin<AlbumScreen>,
|
||||
CollapsingHeaderScrollMixin<AlbumScreen> {
|
||||
final _historySnapshot = TrackHistorySnapshot();
|
||||
List<Track>? _tracks;
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
@@ -543,13 +545,10 @@ class _AlbumScreenState extends ConsumerState<AlbumScreen>
|
||||
ColorScheme colorScheme,
|
||||
List<Track> tracks,
|
||||
) {
|
||||
final historyLookups = tracks
|
||||
.map(historyLookupForTrack)
|
||||
.toList(growable: false);
|
||||
_historySnapshot.update(tracks);
|
||||
final historyLookups = _historySnapshot.lookups;
|
||||
final existingHistoryKeys = ref.watch(
|
||||
downloadHistoryVisibleBatchExistsProvider(
|
||||
HistoryBatchLookupRequest(historyLookups),
|
||||
),
|
||||
downloadHistoryVisibleBatchExistsProvider(_historySnapshot.request),
|
||||
);
|
||||
return SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: wideListInset(context)),
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/screens/track_history_snapshot.dart';
|
||||
import 'package:spotiflac_android/widgets/album_detail_header.dart';
|
||||
import 'package:spotiflac_android/theme/cover_palette.dart';
|
||||
import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
|
||||
@@ -120,6 +121,7 @@ class ArtistScreen extends ConsumerStatefulWidget {
|
||||
class _ArtistScreenState extends ConsumerState<ArtistScreen>
|
||||
with SelectionModeMixin<ArtistScreen> {
|
||||
bool _isLoadingDiscography = false;
|
||||
final _historySnapshot = TrackHistorySnapshot();
|
||||
List<ArtistAlbum>? _albums;
|
||||
List<ArtistAlbum>? _releases;
|
||||
List<Track>? _topTracks;
|
||||
|
||||
@@ -311,13 +311,10 @@ extension _ArtistScreenSections on _ArtistScreenState {
|
||||
}
|
||||
|
||||
final tracks = _topTracks!;
|
||||
final historyLookups = tracks
|
||||
.map(historyLookupForTrack)
|
||||
.toList(growable: false);
|
||||
_historySnapshot.update(tracks);
|
||||
final historyLookups = _historySnapshot.lookups;
|
||||
final existingHistoryKeys = ref.watch(
|
||||
downloadHistoryVisibleBatchExistsProvider(
|
||||
HistoryBatchLookupRequest(historyLookups),
|
||||
),
|
||||
downloadHistoryVisibleBatchExistsProvider(_historySnapshot.request),
|
||||
);
|
||||
const tracksPerPage = 5;
|
||||
final pageCount = (tracks.length / tracksPerPage).ceil();
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/screens/track_history_snapshot.dart';
|
||||
import 'package:spotiflac_android/theme/app_tokens.dart';
|
||||
import 'package:spotiflac_android/widgets/app_bottom_sheet.dart';
|
||||
import 'package:spotiflac_android/widgets/app_sliver_header.dart';
|
||||
@@ -60,6 +61,7 @@ class HomeTab extends ConsumerStatefulWidget {
|
||||
|
||||
class _HomeTabState extends ConsumerState<HomeTab>
|
||||
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
|
||||
final _historySnapshot = TrackHistorySnapshot();
|
||||
final _urlController = TextEditingController();
|
||||
final FocusNode _searchFocusNode = FocusNode();
|
||||
String? _lastSearchQuery;
|
||||
|
||||
@@ -403,13 +403,10 @@ extension _HomeTabSearchResultsUI on _HomeTabState {
|
||||
}
|
||||
|
||||
if (sortedTracks.isNotEmpty) {
|
||||
final historyLookups = sortedTracks
|
||||
.map(historyLookupForTrack)
|
||||
.toList(growable: false);
|
||||
_historySnapshot.update(sortedTracks);
|
||||
final historyLookups = _historySnapshot.lookups;
|
||||
final existingHistoryKeys = ref.watch(
|
||||
downloadHistoryVisibleBatchExistsProvider(
|
||||
HistoryBatchLookupRequest(historyLookups),
|
||||
),
|
||||
downloadHistoryVisibleBatchExistsProvider(_historySnapshot.request),
|
||||
);
|
||||
slivers.addAll(
|
||||
_buildVirtualizedResultSection(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/download_history_provider.dart';
|
||||
import 'package:spotiflac_android/screens/track_history_snapshot.dart';
|
||||
import 'package:spotiflac_android/providers/library_collections_provider.dart';
|
||||
import 'package:spotiflac_android/services/history_database.dart';
|
||||
|
||||
@@ -9,17 +9,14 @@ class LibraryTracksFolderData {
|
||||
List<CollectionTrackEntry>? _entries;
|
||||
Set<String> keys = const {};
|
||||
List<Track> tracks = const [];
|
||||
HistoryBatchLookupRequest historyRequest = const HistoryBatchLookupRequest(
|
||||
[],
|
||||
);
|
||||
final _history = TrackHistorySnapshot();
|
||||
HistoryBatchLookupRequest get historyRequest => _history.request;
|
||||
|
||||
void update(List<CollectionTrackEntry> entries) {
|
||||
if (identical(entries, _entries)) return;
|
||||
_entries = entries;
|
||||
keys = Set.unmodifiable(entries.map((entry) => entry.key));
|
||||
tracks = List.unmodifiable(entries.map((entry) => entry.track));
|
||||
historyRequest = HistoryBatchLookupRequest.snapshot(
|
||||
tracks.map(historyLookupForTrack),
|
||||
);
|
||||
_history.update(tracks);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:spotiflac_android/screens/track_history_snapshot.dart';
|
||||
import 'package:spotiflac_android/widgets/album_detail_header.dart';
|
||||
import 'package:spotiflac_android/screens/selection_mode_mixin.dart';
|
||||
import 'package:spotiflac_android/widgets/collection_scaffold.dart';
|
||||
@@ -54,6 +55,7 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen>
|
||||
with
|
||||
SelectionModeMixin<PlaylistScreen>,
|
||||
CollapsingHeaderScrollMixin<PlaylistScreen> {
|
||||
final _historySnapshot = TrackHistorySnapshot();
|
||||
List<Track>? _fetchedTracks;
|
||||
bool _isLoading = false;
|
||||
String? _error;
|
||||
@@ -396,13 +398,10 @@ class _PlaylistScreenState extends ConsumerState<PlaylistScreen>
|
||||
);
|
||||
}
|
||||
|
||||
final historyLookups = _tracks
|
||||
.map(historyLookupForTrack)
|
||||
.toList(growable: false);
|
||||
_historySnapshot.update(_tracks);
|
||||
final historyLookups = _historySnapshot.lookups;
|
||||
final existingHistoryKeys = ref.watch(
|
||||
downloadHistoryVisibleBatchExistsProvider(
|
||||
HistoryBatchLookupRequest(historyLookups),
|
||||
),
|
||||
downloadHistoryVisibleBatchExistsProvider(_historySnapshot.request),
|
||||
);
|
||||
return SliverPadding(
|
||||
padding: EdgeInsets.symmetric(horizontal: wideListInset(context)),
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/download_history_provider.dart';
|
||||
import 'package:spotiflac_android/services/history_database.dart';
|
||||
|
||||
/// Retains derived lookups while the immutable source list is unchanged.
|
||||
/// Callers replace their list for metadata, order, or filter changes.
|
||||
class TrackHistorySnapshot {
|
||||
List<Track>? _source;
|
||||
HistoryBatchLookupRequest request = HistoryBatchLookupRequest.snapshot([]);
|
||||
|
||||
List<HistoryLookupRequest> get lookups => request.tracks;
|
||||
|
||||
void update(List<Track> tracks) {
|
||||
if (identical(tracks, _source)) return;
|
||||
_source = tracks;
|
||||
request = HistoryBatchLookupRequest.snapshot(
|
||||
tracks.map(historyLookupForTrack),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import 'dart:collection';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/screens/track_history_snapshot.dart';
|
||||
|
||||
class _CountingTracks extends ListBase<Track> {
|
||||
final List<Track> values;
|
||||
int reads = 0;
|
||||
_CountingTracks(this.values);
|
||||
@override
|
||||
int get length => values.length;
|
||||
@override
|
||||
set length(int value) => throw UnsupportedError('immutable');
|
||||
@override
|
||||
Track operator [](int index) {
|
||||
reads++;
|
||||
return values[index];
|
||||
}
|
||||
|
||||
@override
|
||||
void operator []=(int index, Track value) =>
|
||||
throw UnsupportedError('immutable');
|
||||
}
|
||||
|
||||
void main() {
|
||||
test('retains lookups without scanning an unchanged 4000-track snapshot', () {
|
||||
final tracks = _CountingTracks(
|
||||
List.generate(
|
||||
4000,
|
||||
(index) => Track(
|
||||
id: '$index',
|
||||
name: 'Song $index',
|
||||
artistName: 'Artist',
|
||||
albumName: 'Album',
|
||||
duration: 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
final data = TrackHistorySnapshot()..update(tracks);
|
||||
final request = data.request;
|
||||
final hash = request.hashCode;
|
||||
final reads = tracks.reads;
|
||||
for (var i = 0; i < 100; i++) {
|
||||
data.update(tracks);
|
||||
expect(identical(data.request, request), isTrue);
|
||||
expect(data.request.hashCode, hash);
|
||||
}
|
||||
expect(tracks.reads, reads);
|
||||
expect(() => data.lookups.clear(), throwsUnsupportedError);
|
||||
});
|
||||
|
||||
test(
|
||||
'replacement metadata, sorting, filtering, and empty lists invalidate',
|
||||
() {
|
||||
const first = Track(
|
||||
id: '1',
|
||||
name: 'First',
|
||||
artistName: 'A',
|
||||
albumName: 'Album',
|
||||
duration: 1,
|
||||
);
|
||||
const second = Track(
|
||||
id: '2',
|
||||
name: 'Second',
|
||||
artistName: 'A',
|
||||
albumName: 'Album',
|
||||
duration: 1,
|
||||
);
|
||||
final data = TrackHistorySnapshot()..update([first, second]);
|
||||
final original = data.request;
|
||||
data.update([first.copyWith(name: 'Edited'), second]);
|
||||
expect(data.request, isNot(original));
|
||||
expect(data.lookups.first.trackName, 'Edited');
|
||||
data.update([second, first]);
|
||||
expect(data.lookups.map((item) => item.spotifyId), ['2', '1']);
|
||||
data.update([second]);
|
||||
expect(data.lookups.single.spotifyId, '2');
|
||||
data.update([]);
|
||||
expect(data.lookups, isEmpty);
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user