/// Search Provider - Search state management library; import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../../../infrastructure/datasources/remote/music_api_service.dart'; import '../../../domain/entities/track.dart'; import '../../../domain/entities/artist.dart'; import '../../../domain/entities/album.dart'; /// Search state class SearchState { final String query; final bool isSearching; final List tracks; final List artists; final List albums; final String? error; const SearchState({ this.query = '', this.isSearching = false, this.tracks = const [], this.artists = const [], this.albums = const [], this.error, }); SearchState copyWith({ String? query, bool? isSearching, List? tracks, List? artists, List? albums, String? error, }) { return SearchState( query: query ?? this.query, isSearching: isSearching ?? this.isSearching, tracks: tracks ?? this.tracks, artists: artists ?? this.artists, albums: albums ?? this.albums, error: error, ); } int get totalResults => tracks.length + artists.length + albums.length; } /// Search notifier with debouncing class SearchNotifier extends StateNotifier { SearchNotifier(this._musicApiService) : super(const SearchState()); final MusicApiService _musicApiService; Timer? _debounceTimer; static const _debounceDuration = Duration(milliseconds: 500); void search(String query) { if (query.trim().isEmpty) { state = const SearchState(); _debounceTimer?.cancel(); return; } _debounceTimer?.cancel(); state = state.copyWith(query: query, isSearching: true); _debounceTimer = Timer(_debounceDuration, () => _performSearch(query)); } Future _performSearch(String query) async { // Store the original query to check for race conditions final originalQuery = query; try { final results = await _musicApiService.search( query, type: 'all', limit: 20, ); // CRITICAL: Only update state if this is still the current search query // This prevents race conditions where old search results overwrite newer ones if (state.query == originalQuery) { state = SearchState( query: query, tracks: (results['tracks'] as List?) ?.map((json) => Track.fromJson(json as Map)) .toList() ?? [], artists: (results['artists'] as List?) ?.map((json) => Artist.fromJson(json as Map)) .toList() ?? [], albums: (results['albums'] as List?) ?.map((json) => Album.fromJson(json as Map)) .toList() ?? [], ); } else { // This search result is stale, ignore it debugPrint('Ignoring stale search results for "$originalQuery" (current: "${state.query}")'); } } catch (e) { // Only update error state if this is still the current query if (state.query == originalQuery) { debugPrint('Search failed for "$originalQuery": $e'); state = SearchState( query: query, error: e.toString(), ); } } finally { // Only clear loading state if this is still the current query if (state.query == originalQuery) { state = state.copyWith(isSearching: false); } } } void clear() { _debounceTimer?.cancel(); state = const SearchState(); } @override void dispose() { _debounceTimer?.cancel(); super.dispose(); } } /// Search provider final searchProvider = StateNotifierProvider((ref) { final musicApiService = ref.watch(musicApiServiceProvider); return SearchNotifier(musicApiService); });