perf(library): reuse collection lookups across selection rebuilds

This commit is contained in:
zarzet
2026-09-06 02:34:49 +07:00
parent 7b87a8ee15
commit ab21de45c1
4 changed files with 120 additions and 10 deletions
@@ -0,0 +1,25 @@
import 'package:spotiflac_android/models/track.dart';
import 'package:spotiflac_android/providers/download_history_provider.dart';
import 'package:spotiflac_android/providers/library_collections_provider.dart';
import 'package:spotiflac_android/services/history_database.dart';
/// Collection providers replace their track list when its contents change.
/// Keep derived data across selection/layout rebuilds of the same snapshot.
class LibraryTracksFolderData {
List<CollectionTrackEntry>? _entries;
Set<String> keys = const {};
List<Track> tracks = const [];
HistoryBatchLookupRequest historyRequest = const HistoryBatchLookupRequest(
[],
);
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),
);
}
}
+7 -10
View File
@@ -28,6 +28,7 @@ import 'package:spotiflac_android/utils/nav_bar_inset.dart';
import 'package:spotiflac_android/utils/cover_art_utils.dart';
import 'package:spotiflac_android/screens/collapsing_header_scroll_mixin.dart';
import 'package:spotiflac_android/screens/selection_mode_mixin.dart';
import 'package:spotiflac_android/screens/library_tracks_folder_data.dart';
import 'package:spotiflac_android/screens/track_metadata_screen.dart';
import 'package:spotiflac_android/widgets/selection_action_button.dart';
import 'package:spotiflac_android/widgets/selection_bottom_bar.dart';
@@ -67,6 +68,7 @@ class _LibraryTracksFolderScreenState
SelectionModeMixin<LibraryTracksFolderScreen>,
CollapsingHeaderScrollMixin<LibraryTracksFolderScreen> {
UserPlaylistCollection? playlist;
final _folderData = LibraryTracksFolderData();
@override
void initState() {
@@ -272,7 +274,8 @@ class _LibraryTracksFolderScreenState
break;
}
pruneSelection(entries.map((e) => e.key).toSet());
_folderData.update(entries);
pruneSelection(_folderData.keys);
final title = switch (widget.mode) {
LibraryTracksFolderMode.wishlist => context.l10n.collectionWishlist,
@@ -297,16 +300,10 @@ class _LibraryTracksFolderScreenState
LibraryTracksFolderMode.playlist =>
context.l10n.collectionPlaylistEmptySubtitle,
};
final folderTracks = entries
.map((entry) => entry.track)
.toList(growable: false);
final historyLookups = folderTracks
.map(historyLookupForTrack)
.toList(growable: false);
final folderTracks = _folderData.tracks;
final historyLookups = _folderData.historyRequest.tracks;
final existingHistoryKeys = ref.watch(
downloadHistoryVisibleBatchExistsProvider(
HistoryBatchLookupRequest(historyLookups),
),
downloadHistoryVisibleBatchExistsProvider(_folderData.historyRequest),
);
final bottomPadding = MediaQuery.paddingOf(context).bottom;
+23
View File
@@ -48,6 +48,12 @@ class HistoryBatchLookupRequest {
const HistoryBatchLookupRequest(this.tracks);
/// An immutable request whose hash is computed only once. Use for lists
/// retained across widget builds; the legacy constructor remains available.
factory HistoryBatchLookupRequest.snapshot(
Iterable<HistoryLookupRequest> tracks,
) = _HistoryBatchLookupSnapshot;
@override
bool operator ==(Object other) {
if (identical(this, other)) return true;
@@ -65,6 +71,23 @@ class HistoryBatchLookupRequest {
int get hashCode => Object.hashAll(tracks);
}
class _HistoryBatchLookupSnapshot extends HistoryBatchLookupRequest {
_HistoryBatchLookupSnapshot(Iterable<HistoryLookupRequest> tracks)
: super(List.unmodifiable(tracks));
late final int _hash = Object.hashAll(tracks);
@override
bool operator ==(Object other) =>
identical(this, other) ||
other is HistoryBatchLookupRequest &&
hashCode == other.hashCode &&
super == other;
@override
int get hashCode => _hash;
}
class HistoryDatabase {
// The FTS table is a derived, optional index and is initialized lazily after
// the existing schema migration. Keep this contract at v13 because the