fix: user liked tracks memory leak due to isStale & updateQueryFn

This commit is contained in:
Kingkor Roy Tirtho 2023-09-08 10:56:03 +06:00
parent 1b52de0906
commit 57281faa42
4 changed files with 103 additions and 58 deletions

View File

@ -29,7 +29,7 @@ class HeartButton extends HookConsumerWidget {
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final auth = ref.watch(AuthenticationNotifier.provider); final auth = ref.watch(AuthenticationNotifier.provider);
if (auth == null) return Container(); if (auth == null) return const SizedBox.shrink();
return IconButton( return IconButton(
tooltip: tooltip, tooltip: tooltip,
@ -57,18 +57,21 @@ class HeartButton extends HookConsumerWidget {
} }
} }
({ typedef UseTrackToggleLike = ({
bool isLiked, bool isLiked,
Mutation<bool, dynamic, bool> toggleTrackLike, Mutation<bool, dynamic, bool> toggleTrackLike,
Query<User?, dynamic> me, Query<User?, dynamic> me,
}) useTrackToggleLike(Track track, WidgetRef ref) { });
UseTrackToggleLike useTrackToggleLike(Track track, WidgetRef ref) {
final me = useQueries.user.me(ref); final me = useQueries.user.me(ref);
final savedTracks = final savedTracks = useQueries.playlist.likedTracksQuery(ref);
useQueries.playlist.tracksOfQuery(ref, "user-liked-tracks");
final isLiked = final isLiked = useMemoized(
savedTracks.data?.any((element) => element.id == track.id) ?? false; () => savedTracks.data?.any((element) => element.id == track.id) ?? false,
[savedTracks.data, track.id],
);
final mounted = useIsMounted(); final mounted = useIsMounted();
@ -76,28 +79,48 @@ class HeartButton extends HookConsumerWidget {
ref, ref,
track.id!, track.id!,
onMutate: (isLiked) { onMutate: (isLiked) {
print("Toggle Like onMutate: $isLiked");
if (isLiked) {
savedTracks.setData(
savedTracks.data
?.where((element) => element.id != track.id)
.toList() ??
[],
);
} else {
savedTracks.setData( savedTracks.setData(
[ [
if (isLiked == true) ...?savedTracks.data,
...?savedTracks.data?.where((element) => element.id != track.id) track,
else
...?savedTracks.data?..add(track)
], ],
); );
}
return isLiked; return isLiked;
}, },
onData: (data, recoveryData) async { onData: (data, recoveryData) async {
print("Toggle Like onData: $data");
await savedTracks.refresh(); await savedTracks.refresh();
}, },
onError: (payload, isLiked) { onError: (payload, isLiked) {
print("Toggle Like onError: $payload");
if (!mounted()) return; if (!mounted()) return;
savedTracks.setData([ if (isLiked != true) {
if (isLiked != true) savedTracks.setData(
...?savedTracks.data?.where((element) => element.id != track.id) savedTracks.data
else ?.where((element) => element.id != track.id)
...?savedTracks.data?..add(track), .toList() ??
]); [],
);
} else {
savedTracks.setData(
[
...?savedTracks.data,
track,
],
);
}
}, },
); );
@ -113,21 +136,21 @@ class TrackHeartButton extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, ref) { Widget build(BuildContext context, ref) {
final savedTracks = final savedTracks = useQueries.playlist.likedTracksQuery(ref);
useQueries.playlist.tracksOfQuery(ref, "user-liked-tracks"); final (:me, :isLiked, :toggleTrackLike) = useTrackToggleLike(track, ref);
final toggler = useTrackToggleLike(track, ref);
if (toggler.me.isLoading || !toggler.me.hasData) { if (me.isLoading || !me.hasData) {
return const CircularProgressIndicator(); return const CircularProgressIndicator();
} }
return HeartButton( return HeartButton(
tooltip: toggler.isLiked tooltip: isLiked
? context.l10n.remove_from_favorites ? context.l10n.remove_from_favorites
: context.l10n.save_as_favorite, : context.l10n.save_as_favorite,
isLiked: toggler.isLiked, isLiked: isLiked,
onPressed: savedTracks.hasData onPressed: savedTracks.hasData
? () { ? () {
toggler.toggleTrackLike.mutate(toggler.isLiked); toggleTrackLike.mutate(isLiked);
} }
: null, : null,
); );

View File

@ -55,7 +55,12 @@ class PlaylistView extends HookConsumerWidget {
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final meSnapshot = useQueries.user.me(ref); final meSnapshot = useQueries.user.me(ref);
final tracksSnapshot = useQueries.playlist.tracksOfQuery(ref, playlist.id!); final playlistTrackSnapshot =
useQueries.playlist.tracksOfQuery(ref, playlist.id!);
final likedTracksSnapshot = useQueries.playlist.likedTracksQuery(ref);
final tracksSnapshot = playlist.id! == "user-liked-tracks"
? likedTracksSnapshot
: playlistTrackSnapshot;
final isPlaylistPlaying = useMemoized( final isPlaylistPlaying = useMemoized(
() => proxyPlaylist.collections.contains(playlist.id!), () => proxyPlaylist.collections.contains(playlist.id!),

View File

@ -1,4 +1,5 @@
import 'dart:io'; import 'dart:io';
import 'dart:math';
import 'package:catcher/catcher.dart'; import 'package:catcher/catcher.dart';
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
@ -145,23 +146,47 @@ class PlaylistQueries {
); );
} }
Future<List<Track>> likedTracks(
SpotifyApi spotify,
WidgetRef ref,
) async {
final tracks = await spotify.tracks.me.saved.all();
return tracks.map((e) => e.track!).toList();
}
Query<List<Track>, dynamic> likedTracksQuery(WidgetRef ref) {
final query = useCallback((spotify) => likedTracks(spotify, ref), []);
final context = useContext();
return useSpotifyQuery<List<Track>, dynamic>(
"user-liked-tracks",
query,
jsonConfig: JsonConfig(
toJson: (tracks) => <String, dynamic>{
'tracks': tracks.map((e) => e.toJson()).toList(),
},
fromJson: (json) => (json['tracks'] as List)
.map(
(e) => Track.fromJson((e as Map).castKeyDeep<String>()),
)
.toList(),
),
refreshConfig: RefreshConfig.withDefaults(
context,
// will never make it stale
staleDuration: const Duration(days: 60),
),
ref: ref,
);
}
Future<List<Track>> tracksOf( Future<List<Track>> tracksOf(
String playlistId, String playlistId,
SpotifyApi spotify, SpotifyApi spotify,
WidgetRef ref, WidgetRef ref,
) { ) async {
if (playlistId == "user-liked-tracks") { if (playlistId == "user-liked-tracks") return <Track>[];
return spotify.tracks.me.saved
.all()
.then(
(tracks) => tracks.map((e) => e.track!).toList(),
)
.catchError((e) {
final isLoggedIn = ref.read(AuthenticationNotifier.provider) != null;
if (e is SocketException && isLoggedIn) return <Track>[];
throw e;
});
}
return spotify.playlists.getTracksByPlaylistId(playlistId).all().then( return spotify.playlists.getTracksByPlaylistId(playlistId).all().then(
(value) => value.toList(), (value) => value.toList(),
); );
@ -174,22 +199,6 @@ class PlaylistQueries {
return useSpotifyQuery<List<Track>, dynamic>( return useSpotifyQuery<List<Track>, dynamic>(
"playlist-tracks/$playlistId", "playlist-tracks/$playlistId",
(spotify) => tracksOf(playlistId, spotify, ref), (spotify) => tracksOf(playlistId, spotify, ref),
jsonConfig: playlistId == "user-liked-tracks"
? JsonConfig(
toJson: (tracks) => <String, dynamic>{
'tracks': tracks.map((e) => e.toJson()).toList()
},
fromJson: (json) => (json['tracks'] as List)
.map((e) => Track.fromJson(
(e as Map).castKeyDeep<String>(),
))
.toList(),
)
: null,
retryConfig: RetryConfig.withConstantDefaults(
maxRetries: 1,
retryDelay: const Duration(seconds: 5),
),
ref: ref, ref: ref,
); );
} }

View File

@ -1,4 +1,5 @@
import 'package:fl_query/fl_query.dart'; import 'package:fl_query/fl_query.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:spotify/spotify.dart'; import 'package:spotify/spotify.dart';
import 'package:spotube/hooks/use_spotify_query.dart'; import 'package:spotube/hooks/use_spotify_query.dart';
@ -8,6 +9,8 @@ import 'package:spotube/utils/type_conversion_utils.dart';
class UserQueries { class UserQueries {
const UserQueries(); const UserQueries();
Query<User?, dynamic> me(WidgetRef ref) { Query<User?, dynamic> me(WidgetRef ref) {
final context = useContext();
return useSpotifyQuery<User, dynamic>( return useSpotifyQuery<User, dynamic>(
"current-user", "current-user",
(spotify) async { (spotify) async {
@ -26,6 +29,11 @@ class UserQueries {
} }
return me; return me;
}, },
refreshConfig: RefreshConfig.withDefaults(
context,
// will never make it stale
staleDuration: const Duration(days: 60),
),
ref: ref, ref: ref,
); );
} }