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’s AudioPlayerHandler is the glue between just_audio’s raw playback engine and the operating system’s media session infrastructure. It extends BaseAudioHandler with SeekHandler (from the audio_service package) and is registered at app startup via AudioService.init, giving the system everything it needs to display a persistent notification, respond to Bluetooth headset buttons, and honour lock-screen transport controls. This page covers the handler’s constructor, every public method, the notification control set it builds, the Android channel configuration, and how the onFavoriteToggle callback is wired up from the dependency-injection layer.

Registration

AudioPlayerHandler is instantiated inside AudioService.init in main.dart. The Android notification channel is configured at that point:
final audioHandler = await AudioService.init<AudioPlayerHandler>(
  builder: () => AudioPlayerHandler(AudioPlayer()),
  config: const AudioServiceConfig(
    androidNotificationChannelId: 'com.cesararellano.musynth.audio',
    androidNotificationChannelName: 'Musynth',
    androidNotificationOngoing: true,
    androidNotificationIcon: 'mipmap/launcher_icon',
  ),
);
androidNotificationOngoing: true keeps the notification visible while music is playing. The user can dismiss it via the custom Close action, which calls stop() through the handler rather than swiping it away directly.

Constructor

AudioPlayerHandler(AudioPlayer player) : _player = player {
  _player.playbackEventStream.listen(
    (event) => playbackState.add(_buildState(event)),
  );
}
The constructor accepts a just_audio AudioPlayer instance (provided by GetIt) and immediately subscribes to playbackEventStream. Every PlaybackEvent emitted by just_audio is mapped to an audio_service PlaybackState and broadcast on the playbackState stream — the stream that audio_service reads to refresh the notification and media session.

Public Methods

void setQueueAndItem(List<MediaItem> items, int initialIndex)
Pushes a new queue to BaseAudioHandler.queue and sets the active mediaItem to items[initialIndex]. Called by JustAudioPlaybackService.loadPlaylist every time a new playlist is loaded.
void setCurrentItem(MediaItem item)
Adds item to BaseAudioHandler.mediaItem, updating the notification artwork, title, and artist whenever the player advances to the next track (driven by the currentIndexStream subscription in JustAudioPlaybackService).
void updateFavoriteState(bool isFavorite)
Rebuilds the notification control list with the correct heart icon (drawable/ic_favorite or drawable/ic_favorite_border) and emits the updated PlaybackState. Short-circuits with an early return if _isFavorite hasn’t actually changed, avoiding redundant rebuilds.
Future<void> play()     => _player.play();
Future<void> pause()    => _player.pause();
Future<void> seek(Duration position) => _player.seek(position);

Future<void> stop() async {
  await _player.stop();
  await super.stop(); // pushes AudioProcessingState.idle → dismisses notification
}
stop() deliberately calls super.stop() after the player stops. The base-class implementation pushes AudioProcessingState.idle onto playbackState, which is what tells audio_service to stop the Android foreground service and dismiss the notification. Calling only _player.stop() would not reliably produce that event.
Future<void> skipToNext()             => _player.seekToNext();
Future<void> skipToPrevious()         => _player.seekToPrevious();
Future<void> skipToQueueItem(int index) =>
    _player.seek(Duration.zero, index: index);
These delegate directly to just_audio. skipToQueueItem is used by the queue screen to jump to any position in the playlist without rebuilding the source list.
Future<void> customAction(String name, [Map<String, dynamic>? extras]) async {
  switch (name) {
    case 'favorite':
      onFavoriteToggle?.call();
      updateFavoriteState(!_isFavorite);
    case 'close':
      await stop();
  }
}
Handles the two custom notification buttons:
Action nameBehaviour
'favorite'Fires onFavoriteToggle (set by DI layer), then flips the local _isFavorite flag and redraws the notification icon.
'close'Calls stop(), which dismisses the notification via super.stop(). In-app queue and song state are preserved so playback can be resumed from inside the app.

Notification Controls

_buildControls() returns a five-element list of MediaControl objects that maps to the notification’s action bar:
List<MediaControl> _buildControls() => [
  MediaControl.skipToPrevious,
  if (_player.playing) MediaControl.pause else MediaControl.play,
  MediaControl.skipToNext,
  MediaControl.custom(
    androidIcon: _isFavorite
        ? 'drawable/ic_favorite'
        : 'drawable/ic_favorite_border',
    label: _isFavorite ? 'Unfavorite' : 'Favorite',
    name: 'favorite',
  ),
  MediaControl.custom(
    androidIcon: 'drawable/ic_close',
    label: 'Close',
    name: 'close',
  ),
];
androidCompactActionIndices: const [0, 1, 2] (set in _buildState) means only the first three actions — previous, play/pause, next — appear in the compact (collapsed) notification view. The favourite and close actions are visible only when the notification is expanded.

Processing-State Mapping

_buildState(PlaybackEvent event) converts just_audio’s ProcessingState into the audio_service equivalent so the media session reflects accurate buffering and completion states:
PlaybackState _buildState(PlaybackEvent event) {
  final processingState = const {
    ProcessingState.idle:      AudioProcessingState.idle,
    ProcessingState.loading:   AudioProcessingState.loading,
    ProcessingState.buffering: AudioProcessingState.buffering,
    ProcessingState.ready:     AudioProcessingState.ready,
    ProcessingState.completed: AudioProcessingState.completed,
  }[_player.processingState] ?? AudioProcessingState.idle;

  return PlaybackState(
    controls: _buildControls(),
    systemActions: const {MediaAction.seek},
    androidCompactActionIndices: const [0, 1, 2],
    processingState: processingState,
    playing: _player.playing,
    updatePosition: _player.position,
    bufferedPosition: _player.bufferedPosition,
    speed: _player.speed,
    queueIndex: event.currentIndex,
  );
}
systemActions: {MediaAction.seek} tells the Android media session that a seek bar is available, enabling scrubbing from lock-screen controls.

onFavoriteToggle Callback

onFavoriteToggle is a VoidCallback? field set by di.dart after both the handler and FavoritesService have been registered in the service locator:
audioHandler.onFavoriteToggle = () {
  final song = playbackStateCubit.state.songPlayed;
  setupLocator<FavoritesService>().toggle(
    song,
    favoritesState: favoritesCubit.state,
    allSongs: libraryCubit.state.songList,
  );
};
This avoids a circular dependency: the handler doesn’t import FavoritesService; it only holds a nullable callback that is satisfied externally once everything is ready. If the callback is null (e.g. in tests), the tap on the heart icon is silently ignored.
Because onFavoriteToggle is wired up in setupServiceLayer, it is guaranteed to be non-null by the time the user can interact with the notification — setupServiceLayer completes before the first frame renders.

Build docs developers (and LLMs) love