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 uses go_router for all in-app navigation. A single GoRouter instance (AppRouter.router) is created at compile time and passed directly to MaterialApp.router. Every destination is a named route defined in the AppRoutes abstract class, so call-sites use string constants rather than raw path strings.

AppRoutes Constants

abstract final class AppRoutes {
  static const home       = 'home';
  static const search     = 'search';
  static const songPlayed = 'songPlayed';
  static const queue      = 'queue';
  static const album      = 'album';
  static const artist     = 'artist';
  static const genre      = 'genre';
  static const playlist   = 'playlist';
}

Route Table

NamePathScreenTransitionExtra / Path Params
home/HomeScreenFade
search/searchMusicSearchScreenFade
songPlayed/song-playedSongPlayedScreenSlide up (opaque: false)extra: { 'isPlaylist': bool, 'playlistId': int? }
queue/queuePlayingQueueScreenFade
album/album/:idAlbumSelectedScreenFadeextra: AlbumModel, path param :id
artist/artist/:idArtistSelectedScreenFadeextra: ArtistModel, path param :id
genre/genre/:idGenreSelectedScreenFadeextra: GenreModel, path param :id
playlist/playlist/:idPlaylistSelectedScreenFadeextra: PlaylistModel, path param :id
The extra object is the primary mechanism for passing model data to detail screens. Path parameters (:id) are present for deep-link compatibility but the screen reads the full model from state.extra rather than re-querying by ID.

Transition Animations

AppRouter defines two private factory methods — _fade and _slide — that return a GoRouterPageBuilder wrapping a CustomTransitionPage.

Fade (all routes except /song-played)

static GoRouterPageBuilder _fade(
    Widget Function(BuildContext, GoRouterState) build) {
  return (context, state) => CustomTransitionPage<void>(
        key: state.pageKey,
        child: build(context, state),
        transitionDuration: const Duration(milliseconds: 250),
        reverseTransitionDuration: const Duration(milliseconds: 250),
        transitionsBuilder: (_, animation, _, child) => FadeTransition(
          opacity: CurvedAnimation(
            parent: animation,
            curve: Curves.easeOutCubic,
            reverseCurve: Curves.easeInCubic,
          ),
          child: child,
        ),
      );
}
  • Duration: 250 ms forward and reverse
  • Curve: easeOutCubic (enter) / easeInCubic (exit)
  • Widget: FadeTransition on the opacity property

Slide Up (/song-played)

static GoRouterPageBuilder _slide(
    Widget Function(BuildContext, GoRouterState) build,
    {bool opaque = false}) {
  return (context, state) => CustomTransitionPage<void>(
        key: state.pageKey,
        opaque: opaque,             // false → route beneath stays painted
        child: build(context, state),
        transitionDuration: const Duration(milliseconds: 250),
        reverseTransitionDuration: const Duration(milliseconds: 250),
        transitionsBuilder: (_, animation, _, child) {
          final curved = CurvedAnimation(
            parent: animation,
            curve: Curves.easeOutCubic,
            reverseCurve: Curves.easeInCubic,
          );
          return SlideTransition(
            position: Tween<Offset>(
              begin: const Offset(0, 1),  // starts below the viewport
              end: Offset.zero,
            ).animate(curved),
            child: child,
          );
        },
      );
}
  • Duration: 250 ms forward and reverse
  • Curve: easeOutCubic (enter) / easeInCubic (exit)
  • Widget: SlideTransition from Offset(0, 1)Offset(0, 0) (bottom-to-top)
  • opaque: false — the route below remains rendered during the transition, allowing the player sheet to appear to rise over the current screen
The slide route deliberately uses no FadeTransition. Alpha-blending both screens simultaneously would ghost the content together. The pure geometry slide keeps the visual clean.

Simple push (no parameters)

// Navigate to the Now Playing screen from a song list
context.pushNamed(AppRoutes.songPlayed);

// Navigate to the playing queue
context.pushNamed(AppRoutes.queue);

Push with extra map (songPlayed)

context.pushNamed(
  AppRoutes.songPlayed,
  extra: {
    'isPlaylist': true,
    'playlistId': playlist.id,
  },
);
The receiving route reads extra from GoRouterState:
pageBuilder: _slide((_, state) {
  final extra = state.extra as Map<String, dynamic>? ?? {};
  return SongPlayedScreen(
    isPlaylist: extra['isPlaylist'] as bool? ?? false,
    playlistId: extra['playlistId'] as int?,
  );
}, opaque: false),

Push with path parameter + extra model (album, artist, genre, playlist)

// Navigate to an album detail screen
context.pushNamed(
  AppRoutes.album,
  pathParameters: {'id': album.id.toString()},
  extra: album,  // AlbumModel — read directly by AlbumSelectedScreen
);

// Navigate to an artist detail screen
context.pushNamed(
  AppRoutes.artist,
  pathParameters: {'id': artist.id.toString()},
  extra: artist, // ArtistModel
);
The detail screen receives the full model via state.extra, avoiding a redundant repository lookup on navigation:
pageBuilder: _fade((_, state) =>
    AlbumSelectedScreen(albumSelected: state.extra as AlbumModel)),

Router Configuration

AppRouter.router is declared as a static final field and referenced directly in MaterialApp.router:
// main.dart → MyApp.build()
return MultiBlocProvider(
  providers: [...],
  child: MaterialApp.router(
    routerConfig: AppRouter.router,
    title: 'Musynth',
    theme: AppTheme.darkTheme,
    ...
  ),
);
The router starts at initialLocation: '/', which resolves to HomeScreen via the home route.

Build docs developers (and LLMs) love