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 stores album artwork as individual JPEG files on disk rather than decoding images on-the-fly from the media store. This approach makes artwork available across app restarts, allows any widget to load a cover with a standard Image.file call, and avoids repeated native bitmap decodes while scrolling. This page explains how the cache is built, where files live, how the in-memory FileImageCache layer sits in front of it, and when a full rebuild is triggered.

First-Launch Detection

PreferencesRepository exposes an isNotFirstTime boolean backed by SharedPreferences. When this flag is false — meaning the app has never completed a full library scan — LibraryCubit.getAllSongs() sets createArtworks = true, signalling that artwork files must be generated for every album.
// lib/cubits/library/library_cubit.dart
Future<void> getAllSongs({bool forceCreatingArtworks = false}) async {
  bool createArtworks = forceCreatingArtworks;
  final appDirectory = await _artwork.resolveAppDirectory();

  if (!_prefs.isNotFirstTime) {
    createArtworks = true; // first launch — build full cache
  }

  emit(state.copyWith(
    appDirectory: appDirectory,
    isCreatingArtworks: createArtworks,
    isLoading: true,
    isLoadingCatalogue: true,
  ));
  // ...
}
ArtworkCacheService.buildArtworkCache sets _prefs.isNotFirstTime = true once the build completes, so subsequent cold launches skip straight to the incremental check.

Resolving appDirectory

The artwork directory is the app’s documents directory, resolved once via path_provider and persisted in SharedPreferences so every cubit and widget can access it synchronously without hitting the platform channel again.
// lib/data/services/artwork_cache_service.dart
Future<String> resolveAppDirectory() async {
  final dir = (await getApplicationDocumentsDirectory()).path;
  _prefs.appDirectory = dir;
  return dir;
}
LibraryState.appDirectory holds the resolved path. Widgets build the full file path as '${libraryState.appDirectory}/${song.albumId}.jpg'.

Building the Cache

ArtworkCacheService.buildArtworkCache is the single entry point for generating artwork files. It runs on the main isolate because queryArtwork() is a platform channel call — it cannot be moved to a background isolate.
// lib/data/services/artwork_cache_service.dart
Future<void> buildArtworkCache({
  required List<SongModel> songs,
  required String appDirectory,
  required bool force,
}) async {
  if (!force && _prefs.numberOfSongs == songs.length) return;

  // One file per album; skip albums that are already on disk.
  final seenAlbumIds = <Object?>{};
  final pending = <SongModel>[];
  for (final song in songs) {
    if (!seenAlbumIds.add(song.albumId)) continue;
    if (File('$appDirectory/${song.albumId}.jpg').existsSync()) continue;
    pending.add(song);
  }

  // Bounded worker pool — up to 6 artwork fetches in flight at once.
  int next = 0;
  Future<void> worker() async {
    while (next < pending.length) {
      final song = pending[next++];
      final bytes = await _artwork.queryArtwork(song.id);
      if (bytes == null) continue;
      try {
        await File('$appDirectory/${song.albumId}.jpg').writeAsBytes(bytes);
      } catch (_) {}
    }
  }

  final workerCount = pending.length < _maxConcurrentArtworkFetches
      ? pending.length
      : _maxConcurrentArtworkFetches;
  await Future.wait([for (var i = 0; i < workerCount; i++) worker()]);

  _prefs.isNotFirstTime = true;
}

File naming

Every artwork file is named <albumId>.jpg. The album ID is the stable, app-wide key for artwork — all widgets and cubits use it to build the path.

Incremental builds

If force is false and the song count hasn’t changed, the method exits immediately. Only missing files are written — already-cached albums are skipped after an existsSync() check.

Concurrency cap

A worker-pool pattern with _maxConcurrentArtworkFetches = 6 caps native memory pressure from simultaneous bitmap decodes during first launch.

Completion flag

_prefs.isNotFirstTime = true is set only after all pending artwork has been written, so a crash or force-quit mid-build triggers a full retry on the next launch.

LibraryState.isCreatingArtworks

LibraryState exposes an isCreatingArtworks boolean that is true from the moment the cache build starts until it finishes. The UI uses this flag to show feedback:
  • FAB on HomeScreen: displays a circular progress indicator instead of the normal icon while artwork is building
  • Any screen can read context.watch<LibraryCubit>().state.isCreatingArtworks to suppress artwork-dependent UI or show a loading overlay
// LibraryState fields
final bool isCreatingArtworks;

// Set true before build begins
emit(state.copyWith(isCreatingArtworks: createArtworks));

// Cleared after buildArtworkCache completes
emit(state.copyWith(isCreatingArtworks: false));

FileImageCache — In-Memory Provider Cache

Loading a File and decoding it as a FileImage every time a list scrolls would cause redundant I/O and decoder work. FileImageCache is a singleton that wraps decoded FileImage instances in a Map<String, FileImage> keyed by file path.
// lib/data/cache/file_image_cache.dart
class FileImageCache {
  final Map<String, FileImage> _cache = {};

  FileImage get(String path) => _cache[path] ??= FileImage(File(path));

  void evict(String path) {
    _cache.remove(path)?.evict();
  }

  void clear() {
    for (final image in _cache.values) {
      image.evict();
    }
    _cache.clear();
  }
}
The ??= pattern means each path is instantiated at most once per app session. evict(path) is called after tag edits that replace artwork so the stale provider is removed from both this map and Flutter’s ImageCache.

ArtworkImage — Direct Plugin Query Widget

For contexts where a cached file may not exist yet (e.g., while the cache is still building), ArtworkImage queries artwork bytes directly from the on_audio_query plugin rather than reading a file. It is used as the errorBuilder fallback in the Now Playing carousel:
// Fallback inside _ArtworkCarousel (song_played_screen.dart)
errorBuilder: (_, _, _) => ArtworkImage(
  artworkId: song.id,
  type: ArtworkType.AUDIO,
  width: double.infinity,
  height: widget.height,
  size: 500,
  radius: radius,
),
ArtworkImage does not write to disk — it is a display-only fallback. Once the cache build finishes, Image.file takes over and ArtworkImage is no longer rendered.

Force Rebuild

Passing forceCreatingArtworks: true to getAllSongs bypasses the song-count check and the existsSync skip, regenerating every artwork file from scratch. This is used after the tag editor replaces embedded cover art so the updated image is reflected immediately.
// Trigger full artwork rebuild (e.g., after tag edit)
context.read<LibraryCubit>().getAllSongs(forceCreatingArtworks: true);

Why File Cache, Not Memory Cache

Dart isolates cannot share heap memory with the main isolate. A purely in-memory cache would be invisible to any background isolate. A file cache survives app restarts, can be read by any isolate or widget with a path string, and delegates actual decoding to Flutter’s existing ImageCache (which is shared and LRU-managed by the framework).
The two-layer design — file cache (persistent, path-addressed) + FileImageCache (in-memory FileImage providers) + Flutter ImageCache (decoded pixel cache) — means each album artwork is:
  1. Written to disk once
  2. Wrapped in a FileImage provider once per session
  3. Decoded into pixels once per use by Flutter’s image pipeline

Build docs developers (and LLMs) love