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 standardDocumentation 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.
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.
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.
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.
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.isCreatingArtworksto suppress artwork-dependent UI or show a loading overlay
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.
??= 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:
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
PassingforceCreatingArtworks: 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.
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).FileImageCache (in-memory FileImage providers) + Flutter ImageCache (decoded pixel cache) — means each album artwork is:
- Written to disk once
- Wrapped in a
FileImageprovider once per session - Decoded into pixels once per use by Flutter’s image pipeline