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 manages all runtime state with five flutter_bloc Cubits. Each Cubit owns a single, immutable state class and exposes focused methods that call emit() to produce a new state. Widgets observe state reactively through context.watch and trigger changes through context.read.

Cubit Reference

CubitState ClassResponsibility
PlaybackStateCubitPlaybackStateCurrently playing song, active playlist, shuffle toggle, isPlaying flag
AudioControlCubitAudioControlStateLive playback position (currentDuration) and current track index
LibraryCubitLibraryStateSongs, albums, artists, genres, playlists, and lazy per-collection maps
FavoritesCubitFavoritesStateFavorite songs list, persisted IDs, isFavoriteSong(id) helper
UICubitUIStateDominant album color, hero animation ID, in-memory color cache

State Shapes

class PlaybackState {
  PlaybackState({
    SongModel? songPlayed,
    this.currentPlaylist = const [],
    this.isShuffling = false,
    this.isPlaying = false,
  }) : songPlayed = songPlayed ?? SongModel({'_id': 0});

  final SongModel songPlayed;
  final List<SongModel> currentPlaylist;
  final bool isShuffling;
  final bool isPlaying;
}
PlaybackStateCubit exposes granular update methods (updateSongPlayed, updateCurrentPlaylist, updateIsShuffling, updateIsPlaying) and a clearSongPlayed helper that resets the track while preserving the playlist and shuffle preference.
class AudioControlState {
  const AudioControlState({
    this.currentIndex = 0,
    this.currentDuration = Duration.zero,
  });

  final int currentIndex;
  final Duration currentDuration;
}
Both fields are updated by JustAudioPlaybackService, which subscribes to AudioPlayer.positionStream and AudioPlayer.currentIndexStream at app startup and calls audioControlCubit.updateCurrentDuration(...) / audioControlCubit.updateCurrentIndex(...) on each event.
class LibraryState {
  LibraryState({
    this.isLoading = false,
    this.isLoadingCatalogue = false,
    this.isCreatingArtworks = false,
    this.appDirectory = '',
    this.songList = const [],
    this.albumList = const [],
    this.genreList = const [],
    this.artistList = const [],
    this.playLists = const [],
    this.albumCollection = const {},     // Map<int, List<SongModel>>
    this.artistCollection = const {},    // Map<int, ArtistContentModel>
    this.genreCollection = const {},     // Map<int, List<SongModel>>
    this.playlistCollection = const {},  // Map<int, List<SongModel>>
  });
  // ...
}
LibraryCubit fires all four media-store queries (querySongs, queryAlbums, queryGenres, queryArtists) concurrently. Songs are emitted first so the Songs tab becomes interactive immediately; the rest resolve in-flight. The albumCollection, artistCollection, genreCollection, and playlistCollection maps are populated lazily when a detail screen requests them.
class FavoritesState {
  const FavoritesState({
    this.favoriteList = const [],
    this.favoriteSongList = const [],
  });

  final List<SongModel> favoriteList;       // full SongModel objects
  final List<String> favoriteSongList;      // persisted string IDs

  bool isFavoriteSong(int id) => favoriteSongList.contains(id.toString());
}
IDs are persisted as strings in SharedPreferences. After the library loads, FavoritesCubit.initFavorites(allSongs) runs on an Isolate to reconstruct the full SongModel list from those IDs without blocking the UI thread.
class UIState {
  const UIState({
    this.currentDominantColor,
    this.currentHeroId = '',
    this.dominantColorCollection = const {},
  });

  final Color? currentDominantColor;
  final String currentHeroId;
  final Map<String, String> dominantColorCollection; // albumId → hex string

  Color get dominantColor => currentDominantColor ?? Colors.black;

  // Computed luminance helpers used by SongPlayedScreen
  Color get songPlayedThemeColor => ...;
  Brightness get songPlayedBrightness => ...;
  TextTheme get songPlayedTypography => ...;
}
UICubit subscribes to PlaybackStateCubit.stream in its constructor and automatically calls searchDominantColorByAlbumId whenever the playing song changes. The color is derived natively on the platform side (androidx.palette / Core Image) and cached in dominantColorCollection so subsequent visits to the same album are instant.

Reading State in Widgets

// In build() — rebuilds when state changes
final playbackState = context.watch<PlaybackStateCubit>().state;
final isPlaying = playbackState.isPlaying;

// In callbacks — does NOT rebuild; reads the latest value once
onTap: _handlePlayTap,

Future<void> _handlePlayTap() async {
  final orchestrator = setupLocator<MusicOrchestratorService>();
  final currentSong = context.read<PlaybackStateCubit>().state.songPlayed;
  await orchestrator.playSong(currentSong);
}
Use context.watch only inside build(). Using it in an initState, callback, or outside the widget lifecycle throws a runtime assertion.
When a widget only needs one field from a large state, consider using BlocSelector to rebuild only when that specific field changes, reducing unnecessary repaints.

LibraryCubit: Emit Pattern in Detail

The getAllSongs method illustrates how Cubits emit partial state updates as async work completes:
Future<void> getAllSongs({bool forceCreatingArtworks = false}) async {
  // 1. Signal loading state immediately so the UI can show a spinner.
  emit(state.copyWith(
    isLoading: true,
    isLoadingCatalogue: true,
    isCreatingArtworks: createArtworks,
    appDirectory: appDirectory,
  ));

  // 2. All four queries fire concurrently.
  final songsFuture   = _audio.querySongs();
  final albumsFuture  = _audio.queryAlbums();
  final genresFuture  = _audio.queryGenres();
  final artistsFuture = _audio.queryArtists();

  // 3. Emit songs first — Songs tab is interactive before the rest arrive.
  final songList = await songsFuture;
  emit(state.copyWith(songList: songList, isLoading: false));
  await onSongsLoaded?.call(songList); // notifies FavoritesCubit

  // 4. Await the remaining in-flight futures together.
  final (albumList, genreList, artistList) =
      await (albumsFuture, genresFuture, artistsFuture).wait;
  final playLists = (await playlistsFuture) ?? state.playLists;

  emit(state.copyWith(
    albumList: albumList,
    genreList: genreList,
    artistList: artistList,
    playLists: playLists,
    isLoadingCatalogue: false,
  ));

  // 5. Build artwork cache in the background; clear the flag when done.
  await _artwork.buildArtworkCache(songs: songList, ...);
  emit(state.copyWith(isCreatingArtworks: false));
}
Each emit call is an immutable copyWith snapshot. The widget tree receives only the fields it watches, so intermediate loading states are surfaced incrementally.

MultiBlocProvider Setup

All five Cubits are instantiated in main() before runApp and injected into the widget tree using BlocProvider.value. This pattern hands ownership of each Cubit to main() rather than to a widget subtree, ensuring they survive navigation and are accessible everywhere.
// main.dart — after setupServiceLayer()
runApp(MyApp(
  libraryCubit: libraryCubit,
  playbackStateCubit: playbackStateCubit,
  favoritesCubit: favoritesCubit,
  audioControlCubit: audioControlCubit,
  uiCubit: uiCubit,
));

// MyApp.build()
return MultiBlocProvider(
  providers: [
    BlocProvider.value(value: libraryCubit),
    BlocProvider.value(value: playbackStateCubit),
    BlocProvider.value(value: favoritesCubit),
    BlocProvider.value(value: audioControlCubit),
    BlocProvider.value(value: uiCubit),
  ],
  child: MaterialApp.router(...),
);
Because BlocProvider.value is used (not the default BlocProvider constructor), the Cubits are not closed when the MultiBlocProvider widget is removed from the tree. Their lifetime is managed by the GetIt container and main().

Cubit Lifecycle Summary

1

Instantiation in main()

Each Cubit is created with its required repository/cubit dependencies injected directly from the GetIt locator (e.g., setupLocator<PreferencesRepository>()).
2

Registration in GetIt

setupServiceLayer(...) immediately registers every Cubit as a singleton via setupLocator.registerSingleton<XCubit>(xCubit), making them available to services without a BuildContext.
3

Exposure to widget tree

MultiBlocProvider + BlocProvider.value makes each Cubit available to all descendants of MyApp via context.read / context.watch.
4

Service subscriptions

JustAudioPlaybackService and UICubit subscribe to player streams in their constructors, bridging native audio events into Cubit state emissions automatically.

Build docs developers (and LLMs) love