mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-16 06:45:26 +02:00
perf(library): reuse collection lookups across selection rebuilds
This commit is contained in:
@@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import 'dart:collection';
|
||||
import 'package:spotiflac_android/models/track.dart';
|
||||
import 'package:spotiflac_android/providers/library_collections_provider.dart';
|
||||
import 'package:spotiflac_android/screens/library_tracks_folder_data.dart';
|
||||
import 'package:spotiflac_android/services/history_database.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
class _ReadCountingList<T> extends ListBase<T> {
|
||||
_ReadCountingList(this.values);
|
||||
final List<T> values;
|
||||
int reads = 0;
|
||||
@override
|
||||
int get length => values.length;
|
||||
@override
|
||||
set length(int value) => throw UnsupportedError('immutable');
|
||||
@override
|
||||
T operator [](int index) {
|
||||
reads++;
|
||||
return values[index];
|
||||
}
|
||||
|
||||
@override
|
||||
void operator []=(int index, T value) => throw UnsupportedError('immutable');
|
||||
}
|
||||
|
||||
void main() {
|
||||
TestWidgetsFlutterBinding.ensureInitialized();
|
||||
test('unchanged collection snapshot does no additional full-list reads', () {
|
||||
final entries = _ReadCountingList(
|
||||
List.generate(
|
||||
4000,
|
||||
(index) => CollectionTrackEntry(
|
||||
key: '$index',
|
||||
addedAt: DateTime.utc(2026),
|
||||
track: Track(
|
||||
id: '$index',
|
||||
name: 'Song $index',
|
||||
artistName: 'Artist',
|
||||
albumName: 'Album',
|
||||
duration: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
final data = LibraryTracksFolderData()..update(entries);
|
||||
final request = data.historyRequest;
|
||||
final reads = entries.reads;
|
||||
final hash = request.hashCode;
|
||||
for (var i = 0; i < 100; i++) {
|
||||
data.update(entries);
|
||||
expect(identical(data.historyRequest, request), isTrue);
|
||||
expect(data.historyRequest.hashCode, hash);
|
||||
}
|
||||
expect(entries.reads, reads);
|
||||
expect(data.keys.length, 4000);
|
||||
expect(() => data.tracks.clear(), throwsUnsupportedError);
|
||||
expect(() => request.tracks.clear(), throwsUnsupportedError);
|
||||
expect(request, HistoryBatchLookupRequest(request.tracks));
|
||||
expect(hash, HistoryBatchLookupRequest(request.tracks).hashCode);
|
||||
data.update(entries.take(2).toList());
|
||||
expect(data.keys, {'0', '1'});
|
||||
expect(data.tracks.length, 2);
|
||||
expect(identical(data.historyRequest, request), isFalse);
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user