Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/CesarArellano/Music-Player-App/llms.txt

Use this file to discover all available pages before exploring further.

Musynth includes a full-screen search experience (MusicSearchScreen) that filters your entire local library — songs, albums, and artists — as you type. Results are computed off the main thread with a 150 ms debounce, and a microphone button lets you dictate queries via the speech_to_text package. This page explains the UI, the search algorithm, voice input, and how results are displayed.
Tap the magnifying-glass icon (🔍) in the app bar of the home screen, or from any AlbumSelectedScreen or ArtistSelectedScreen — all three push AppRoutes.search via context.pushNamed(AppRoutes.search). MusicSearchScreen is a custom StatefulWidget, not a SearchDelegate subclass. Its scaffold is transparent so the global AppBackground shows through, matching the home screen’s visual style.

Search field

The text field is a rounded pill shape with:
  • A search icon (Icons.search) as a prefix.
  • A × clear button (Icons.close) as a suffix when the field has text.
  • A mic button (Icons.mic) as a suffix when the field is empty — turns red while listening.
  • An external Cancel button to the right of the pill that pops the screen.
// _SearchField decoration (simplified)
InputDecoration(
  hintText: isListening ? 'Listening...' : 'Search',
  prefixIcon: const Icon(Icons.search, color: hintColor),
  suffixIcon: hasText
      ? IconButton(icon: const Icon(Icons.close), onPressed: onClear)
      : IconButton(
          icon: Icon(
            Icons.mic,
            color: isListening ? Colors.redAccent : hintColor,
          ),
          onPressed: onMicTap,
        ),
)

Debounce

Every keystroke cancels the previous Timer and schedules a new one for 150 ms. An empty field immediately clears the results with no Isolate overhead:
_controller.addListener(() {
  final text = _controller.text;
  if (text == _query) return;
  _debounceTimer?.cancel();
  if (text.isEmpty) {
    setState(() {
      _query = text;
      _searchResult = null;
    });
    return;
  }
  setState(() => _query = text);
  _debounceTimer = Timer(
    const Duration(milliseconds: 150),
    () => _runSearch(text),
  );
});

Off-thread filtering

When the timer fires, _runSearch delegates to LibraryState.searchAsync, which executes the filter logic inside Isolate.run():
Future<void> _runSearch(String query) async {
  final libraryState = context.read<LibraryCubit>().state;
  final result = await LibraryState.searchAsync(
    query: query,
    songList: libraryState.songList,
    albumList: libraryState.albumList,
    artistList: libraryState.artistList,
  );
  if (!mounted || query != _query) return; // stale-result guard
  setState(() => _searchResult = result);
}
The algorithm is O(n) per collection — a single pass using .toLowerCase().contains(query.toLowerCase()) on each item — and it runs entirely off the main thread so scrolling the results list stays smooth.

Stale-result guard

If the user keeps typing while an Isolate is running, the result is silently discarded when it arrives (query != _query). Only the freshest query’s result is ever displayed.
The speech_to_text package powers voice input. The mic button calls _toggleListening():
Future<void> _toggleListening() async {
  if (!_speechEnabled) return;
  if (_speech.isNotListening) {
    FocusScope.of(context).unfocus(); // dismiss keyboard before listening
    await _speech.listen(
      onResult: _onSpeechResult,
      listenOptions: SpeechListenOptions(localeId: 'es-MX'),
    );
  } else {
    await _speech.stop();
  }
  setState(() {});
}
Recognised words are written directly into the TextEditingController, which in turn fires the normal debounce → search pipeline:
void _onSpeechResult(SpeechRecognitionResult result) {
  _controller.value = _controller.value.copyWith(
    text: result.recognizedWords,
    selection: TextSelection.collapsed(
      offset: result.recognizedWords.length,
    ),
  );
}
Voice search initialises asynchronously when the screen opens (_initSpeech()). If SpeechToText.initialize() returns false (microphone permission denied or speech services unavailable), the mic button is silently disabled — no error is surfaced to the user.

Search results

Results arrive as a MultipleSearchModel containing three lists:
FieldTypeContents
songsList<SongModel>Tracks whose title or artist contains the query
albumsList<AlbumModel>Albums whose name contains the query
artistsList<ArtistModel>Artists whose name contains the query
Results are presented in three labelled sections, each preceded by a section header showing the section name and match count:
Each song tile shows artwork, title, and artist. Tapping plays the song immediately as PlaylistType.songs and pushes the Now Playing screen. Long-pressing opens MoreSongOptionsModal.
When the query is empty, or while the initial Isolate result hasn’t arrived yet, the screen shows a large centred music note icon as a placeholder.

Performance summary

Off-thread execution: LibraryState.searchAsync passes the full song, album, and artist lists into Isolate.run(). The filter loop never touches the main thread, so the UI remains responsive.150 ms debounce: Only one Isolate is spawned per “pause” in typing, not per keystroke. For fast typists this can reduce Isolate launches by 5–10× compared to no debounce.Stale-result discard: Overlapping queries never race to update the UI. The screen always shows the result for the most recent query.In-memory data: All three collections (songList, albumList, artistList) are already in LibraryState from the initial scan — no I/O happens during search.

Build docs developers (and LLMs) love