Fliin
On this page

Screens / lists

Album and playlist

Shows an album or a playlist: its cover, title and artist, Play and Shuffle, then its songs, numbered on an album, and the song now playing on the accent at the bottom.

16 of 16 variants pass
Album and playlist, album state
Platform
Brightness
Text scale
Direction

Import

Album and playlist is part of Fliin Pro. With a license key, sign in once with fliin login, then add it in a Flutter app where fliin init has run:

fliin login
fliin add collection

Also installs: app-bar, button, core, empty-state, icons, mini-player, page, result-card, skeleton, tab-bar, text, track-row

Examples

Album

An album: its tracks numbered, the artist named only on a guest's track. Tap a track, "Play" or "Shuffle" to play it.

Album and playlist, album state
import 'dart:math' as math;

import 'package:your_app/ui/fliin/components/tab_bar/fl_tab_bar.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:your_app/ui/fliin/icons/fl_icons.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_controller.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_data.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_screen.dart';
import 'package:flutter/widgets.dart';

class CollectionAlbumExample extends StatefulWidget {
  const CollectionAlbumExample({super.key});

  @override
  State<CollectionAlbumExample> createState() => _CollectionAlbumExampleState();
}

class _CollectionAlbumExampleState extends State<CollectionAlbumExample> {
  final _collection = sampleAlbum(image: _cover);
  final _random = math.Random();
  late final _now = ValueNotifier<CollectionNowPlaying?>(_playing(_collection.tracks.first, playing: true));
  var _tab = 2;

  static CollectionNowPlaying _playing(CollectionTrack track, {required bool playing}) => CollectionNowPlaying(
    trackId: track.id,
    title: track.title,
    artist: track.artist,
    playing: playing,
    artwork: track.artwork,
  );

  void _play(Collection collection, CollectionTrack track) => _now.value = _playing(track, playing: true);

  void _playPause() {
    final now = _now.value;
    if (now == null) return;
    final track = _collection.tracks.firstWhere((t) => t.id == now.trackId);
    _now.value = _playing(track, playing: !now.playing);
  }

  void _next() {
    final tracks = _collection.tracks;
    final index = tracks.indexWhere((t) => t.id == _now.value?.trackId);
    _now.value = _playing(tracks[(index + 1) % tracks.length], playing: true);
  }

  @override
  void dispose() {
    _now.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CollectionScreen.standalone(
      service: SampleCollectionService(image: _cover, latency: const Duration(milliseconds: 900)),
      id: _collection.id,
      initial: CollectionShown(_collection),
      nowPlaying: _now,
      onPlay: _play,
      onPlayAll: (collection) => _play(collection, collection.tracks.first),
      onShuffle: (collection) => _play(collection, collection.tracks[_random.nextInt(collection.tracks.length)]),
      onPlayPause: _playPause,
      onNext: _next,
      onOpenPlayer: () {},
      onMore: (_) {},
      onBack: () {},
      navigation: FlBottomTabBar(
        index: _tab,
        onSelected: (index) => setState(() => _tab = index),
        labels: FlBottomTabLabels.never,
        tabs: [
          FlBottomTab(icon: FlIcon(FlIcons.house), label: 'Home'),
          FlBottomTab(icon: FlIcon(FlIcons.magnifier), label: 'Search'),
          FlBottomTab(icon: FlIcon(FlIcons.bookmark), label: 'Library'),
          FlBottomTab(icon: FlIcon(FlIcons.person), label: 'Profile'),
        ],
      ),
    );
  }
}

/// The sample covers, bundled with the catalog: the same Unsplash photos
/// the registry loads over the network.
ImageProvider _cover(String name) => AssetImage('images/music/$name.jpg');

Playlist

A playlist: its description, each track with its cover and artist, the current one paused.

Album and playlist, playlist state
import 'dart:math' as math;

import 'package:your_app/ui/fliin/components/tab_bar/fl_tab_bar.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:your_app/ui/fliin/icons/fl_icons.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_controller.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_data.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_screen.dart';
import 'package:flutter/widgets.dart';

class CollectionPlaylistExample extends StatefulWidget {
  const CollectionPlaylistExample({super.key});

  @override
  State<CollectionPlaylistExample> createState() => _CollectionPlaylistExampleState();
}

class _CollectionPlaylistExampleState extends State<CollectionPlaylistExample> {
  final _collection = samplePlaylist(image: _cover);
  final _random = math.Random();
  late final _now = ValueNotifier<CollectionNowPlaying?>(_playing(_collection.tracks.first, playing: false));
  var _tab = 2;

  static CollectionNowPlaying _playing(CollectionTrack track, {required bool playing}) => CollectionNowPlaying(
    trackId: track.id,
    title: track.title,
    artist: track.artist,
    playing: playing,
    artwork: track.artwork,
  );

  void _play(Collection collection, CollectionTrack track) => _now.value = _playing(track, playing: true);

  void _playPause() {
    final now = _now.value;
    if (now == null) return;
    final track = _collection.tracks.firstWhere((t) => t.id == now.trackId);
    _now.value = _playing(track, playing: !now.playing);
  }

  void _next() {
    final tracks = _collection.tracks;
    final index = tracks.indexWhere((t) => t.id == _now.value?.trackId);
    _now.value = _playing(tracks[(index + 1) % tracks.length], playing: true);
  }

  @override
  void dispose() {
    _now.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CollectionScreen.standalone(
      service: SampleCollectionService(image: _cover, latency: const Duration(milliseconds: 900)),
      id: _collection.id,
      initial: CollectionShown(_collection),
      nowPlaying: _now,
      onPlay: _play,
      onPlayAll: (collection) => _play(collection, collection.tracks.first),
      onShuffle: (collection) => _play(collection, collection.tracks[_random.nextInt(collection.tracks.length)]),
      onPlayPause: _playPause,
      onNext: _next,
      onOpenPlayer: () {},
      onMore: (_) {},
      onBack: () {},
      navigation: FlBottomTabBar(
        index: _tab,
        onSelected: (index) => setState(() => _tab = index),
        labels: FlBottomTabLabels.never,
        tabs: [
          FlBottomTab(icon: FlIcon(FlIcons.house), label: 'Home'),
          FlBottomTab(icon: FlIcon(FlIcons.magnifier), label: 'Search'),
          FlBottomTab(icon: FlIcon(FlIcons.bookmark), label: 'Library'),
          FlBottomTab(icon: FlIcon(FlIcons.person), label: 'Profile'),
        ],
      ),
    );
  }
}

/// The sample covers, bundled with the catalog: the same Unsplash photos
/// the registry loads over the network.
ImageProvider _cover(String name) => AssetImage('images/music/$name.jpg');

Empty

A new playlist, without songs: no "Play" nor "Shuffle", and a line that says where the songs will show.

Album and playlist, empty state
import 'package:your_app/ui/fliin/screens/collection/collection_controller.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_data.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_screen.dart';
import 'package:flutter/widgets.dart';

class CollectionEmptyExample extends StatefulWidget {
  const CollectionEmptyExample({super.key});

  @override
  State<CollectionEmptyExample> createState() => _CollectionEmptyExampleState();
}

class _CollectionEmptyExampleState extends State<CollectionEmptyExample> {
  final _now = ValueNotifier<CollectionNowPlaying?>(
    CollectionNowPlaying(
      trackId: 'track-1',
      title: 'October Light',
      artist: 'The Tides',
      playing: true,
      artwork: _cover('wild-coast'),
    ),
  );

  void _playPause() {
    final now = _now.value;
    if (now == null) return;
    _now.value = CollectionNowPlaying(
      trackId: now.trackId,
      title: now.title,
      artist: now.artist,
      playing: !now.playing,
      artwork: now.artwork,
    );
  }

  @override
  void dispose() {
    _now.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CollectionScreen.standalone(
      service: SampleCollectionService(image: _cover, latency: const Duration(milliseconds: 900)),
      id: 'playlist-new',
      initial: const CollectionShown(
        Collection(
          id: 'playlist-new',
          kind: CollectionKind.playlist,
          title: 'New playlist',
          byline: 'Lena Moreau',
          tracks: [],
        ),
      ),
      nowPlaying: _now,
      onPlay: (_, _) {},
      onPlayAll: (_) {},
      onShuffle: (_) {},
      onPlayPause: _playPause,
      onOpenPlayer: () {},
      onBack: () {},
    );
  }
}

/// The sample covers, bundled with the catalog: the same Unsplash photos
/// the registry loads over the network.
ImageProvider _cover(String name) => AssetImage('images/music/$name.jpg');

Loading

While the collection loads, a skeleton of its cover, title, buttons and tracks.

Album and playlist, loading state
import 'package:your_app/ui/fliin/screens/collection/collection_controller.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_data.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_screen.dart';
import 'package:flutter/widgets.dart';

class CollectionLoadingExample extends StatefulWidget {
  const CollectionLoadingExample({super.key});

  @override
  State<CollectionLoadingExample> createState() => _CollectionLoadingExampleState();
}

class _CollectionLoadingExampleState extends State<CollectionLoadingExample> {
  final _now = ValueNotifier<CollectionNowPlaying?>(
    CollectionNowPlaying(
      trackId: 'track-1',
      title: 'October Light',
      artist: 'The Tides',
      playing: true,
      artwork: _cover('wild-coast'),
    ),
  );

  void _playPause() {
    final now = _now.value;
    if (now == null) return;
    _now.value = CollectionNowPlaying(
      trackId: now.trackId,
      title: now.title,
      artist: now.artist,
      playing: !now.playing,
      artwork: now.artwork,
    );
  }

  @override
  void dispose() {
    _now.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CollectionScreen.standalone(
      service: SampleCollectionService(image: _cover, latency: const Duration(milliseconds: 900)),
      id: 'album-1',
      initial: const CollectionLoading(),
      nowPlaying: _now,
      onPlay: (_, _) {},
      onPlayAll: (_) {},
      onShuffle: (_) {},
      onPlayPause: _playPause,
      onOpenPlayer: () {},
      onBack: () {},
    );
  }
}

/// The sample covers, bundled with the catalog: the same Unsplash photos
/// the registry loads over the network.
ImageProvider _cover(String name) => AssetImage('images/music/$name.jpg');

Error

When loading fails, the screen says so and offers to try again.

Album and playlist, error state
import 'package:your_app/ui/fliin/screens/collection/collection_controller.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_data.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_screen.dart';
import 'package:flutter/widgets.dart';

class CollectionErrorExample extends StatefulWidget {
  const CollectionErrorExample({super.key});

  @override
  State<CollectionErrorExample> createState() => _CollectionErrorExampleState();
}

class _CollectionErrorExampleState extends State<CollectionErrorExample> {
  final _now = ValueNotifier<CollectionNowPlaying?>(
    CollectionNowPlaying(
      trackId: 'track-1',
      title: 'October Light',
      artist: 'The Tides',
      playing: true,
      artwork: _cover('wild-coast'),
    ),
  );

  void _playPause() {
    final now = _now.value;
    if (now == null) return;
    _now.value = CollectionNowPlaying(
      trackId: now.trackId,
      title: now.title,
      artist: now.artist,
      playing: !now.playing,
      artwork: now.artwork,
    );
  }

  @override
  void dispose() {
    _now.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CollectionScreen.standalone(
      service: SampleCollectionService(image: _cover, latency: const Duration(milliseconds: 900)),
      id: 'album-1',
      initial: const CollectionFailed(),
      nowPlaying: _now,
      onPlay: (_, _) {},
      onPlayAll: (_) {},
      onShuffle: (_) {},
      onPlayPause: _playPause,
      onOpenPlayer: () {},
      onBack: () {},
    );
  }
}

/// The sample covers, bundled with the catalog: the same Unsplash photos
/// the registry loads over the network.
ImageProvider _cover(String name) => AssetImage('images/music/$name.jpg');

Offline

Offline, the screen says so and offers to try again.

Album and playlist, offline state
import 'package:your_app/ui/fliin/screens/collection/collection_controller.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_data.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_screen.dart';
import 'package:flutter/widgets.dart';

class CollectionOfflineExample extends StatefulWidget {
  const CollectionOfflineExample({super.key});

  @override
  State<CollectionOfflineExample> createState() => _CollectionOfflineExampleState();
}

class _CollectionOfflineExampleState extends State<CollectionOfflineExample> {
  final _now = ValueNotifier<CollectionNowPlaying?>(
    CollectionNowPlaying(
      trackId: 'track-1',
      title: 'October Light',
      artist: 'The Tides',
      playing: true,
      artwork: _cover('wild-coast'),
    ),
  );

  void _playPause() {
    final now = _now.value;
    if (now == null) return;
    _now.value = CollectionNowPlaying(
      trackId: now.trackId,
      title: now.title,
      artist: now.artist,
      playing: !now.playing,
      artwork: now.artwork,
    );
  }

  @override
  void dispose() {
    _now.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return CollectionScreen.standalone(
      service: SampleCollectionService(image: _cover, latency: const Duration(milliseconds: 900)),
      id: 'album-1',
      initial: const CollectionOffline(),
      nowPlaying: _now,
      onPlay: (_, _) {},
      onPlayAll: (_) {},
      onShuffle: (_) {},
      onPlayPause: _playPause,
      onOpenPlayer: () {},
      onBack: () {},
    );
  }
}

/// The sample covers, bundled with the catalog: the same Unsplash photos
/// the registry loads over the network.
ImageProvider _cover(String name) => AssetImage('images/music/$name.jpg');

Routing

fliin add collection prints the route for the router it finds in your pubspec, a file next to the page that wires its navigation. Each of them:

go_router

// lib/collection_route.dart, next to collection_page.dart.
import 'package:go_router/go_router.dart';

import 'collection_page.dart';

/// Add collectionRoute to the routes of your GoRouter.
final collectionRoute = GoRoute(
  path: '/collections/:id',
  builder: (context, state) => CollectionPage(
    collectionId: state.pathParameters['id']!,
    // Alone in the stack (deep link, go): your app's home.
    onBack: () => context.canPop() ? context.pop() : context.go('/'),
  ),
);

navigator

// lib/collection_route.dart, next to collection_page.dart.
import 'package:flutter/material.dart';

import 'collection_page.dart';

/// Opens “Album and playlist”: `Navigator.of(context).push(collectionRoute(collectionId))`.
Route<void> collectionRoute(String collectionId) => MaterialPageRoute<void>(
  builder: (context) => CollectionPage(
    collectionId: collectionId,
    onBack: () => Navigator.of(context).maybePop(),
  ),
);

State

The controller is a ChangeNotifier from widgets.dart, with no package. fliin add collection prints the page that holds it with the state management it finds, its imports written for your package; below they read your_app. Your backend implements the Service interface of the screen's _data.dart file:

none

// lib/collection_page.dart
// Your backend: implement CollectionService (collection_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/collection/collection_data.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_screen.dart';
import 'package:your_app/ui/fliin/screens/player/player_data.dart';
import 'package:flutter/widgets.dart';

import 'player_page.dart';

// The mini player shows the app's player, the one of PlayerPage: a
// track played here plays there, and a pause there shows here.
final collectionNowPlaying = CollectionNowPlayingOf(
  samplePlayer.playback,
  (playback) => switch (playback) {
    PlaybackReady(:final track, :final playing, :final buffering) => CollectionNowPlaying(
      trackId: track.id,
      title: track.title,
      artist: track.artist,
      artwork: track.artwork,
      playing: playing,
      buffering: buffering,
    ),
    _ => null,
  },
);

PlayerTrack _playerTrack(CollectionTrack track) => PlayerTrack(
  id: track.id,
  title: track.title,
  artist: track.artist,
  album: track.album,
  duration: track.duration,
  artwork: track.artwork,
);

// The album or the playlist becomes the player's queue: from the tapped
// track, from its first, or shuffled.
void playCollection(Collection collection, {CollectionTrack? from, bool shuffle = false}) => samplePlayer.playQueue(
  [for (final track in collection.tracks) _playerTrack(track)],
  from: from == null ? null : _playerTrack(from),
  shuffle: shuffle,
);

class CollectionPage extends StatelessWidget {
  const CollectionPage({
    super.key,
    required this.collectionId,
    required this.onBack,
  });

  final String collectionId;
  final VoidCallback onBack;

  @override
  Widget build(BuildContext context) => CollectionScreen.standalone(
    service: SampleCollectionService(),
    id: collectionId,
    nowPlaying: collectionNowPlaying,
    onPlay: (collection, track) => playCollection(collection, from: track),
    onPlayAll: playCollection,
    onShuffle: (collection) => playCollection(collection, shuffle: true),
    onPlayPause: samplePlayer.playPause,
    onNext: samplePlayer.next,
    // The player opens out of the dock, under the finger.
    player: (context, close) => PlayerPage(onBack: close),
    onBack: onBack,
  );
}

provider

// lib/collection_page.dart
// Your backend: implement CollectionService (collection_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/collection/collection_controller.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_data.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_screen.dart';
import 'package:your_app/ui/fliin/screens/player/player_data.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';

import 'player_page.dart';

// The mini player shows the app's player, the one of PlayerPage: a
// track played here plays there, and a pause there shows here.
final collectionNowPlaying = CollectionNowPlayingOf(
  samplePlayer.playback,
  (playback) => switch (playback) {
    PlaybackReady(:final track, :final playing, :final buffering) => CollectionNowPlaying(
      trackId: track.id,
      title: track.title,
      artist: track.artist,
      artwork: track.artwork,
      playing: playing,
      buffering: buffering,
    ),
    _ => null,
  },
);

PlayerTrack _playerTrack(CollectionTrack track) => PlayerTrack(
  id: track.id,
  title: track.title,
  artist: track.artist,
  album: track.album,
  duration: track.duration,
  artwork: track.artwork,
);

// The album or the playlist becomes the player's queue: from the tapped
// track, from its first, or shuffled.
void playCollection(Collection collection, {CollectionTrack? from, bool shuffle = false}) => samplePlayer.playQueue(
  [for (final track in collection.tracks) _playerTrack(track)],
  from: from == null ? null : _playerTrack(from),
  shuffle: shuffle,
);

class CollectionPage extends StatelessWidget {
  const CollectionPage({
    super.key,
    required this.collectionId,
    required this.onBack,
  });

  final String collectionId;
  final VoidCallback onBack;

  @override
  Widget build(BuildContext context) => ChangeNotifierProvider(
    create: (_) => CollectionController(service: SampleCollectionService(), id: collectionId)..load(),
    child: Builder(
      builder: (context) => CollectionScreen(
        controller: context.read<CollectionController>(),
        nowPlaying: collectionNowPlaying,
        onPlay: (collection, track) => playCollection(collection, from: track),
        onPlayAll: playCollection,
        onShuffle: (collection) => playCollection(collection, shuffle: true),
        onPlayPause: samplePlayer.playPause,
        onNext: samplePlayer.next,
        // The player opens out of the dock, under the finger.
        player: (context, close) => PlayerPage(onBack: close),
        onBack: onBack,
      ),
    ),
  );
}

riverpod

// lib/collection_page.dart
// Your backend: implement CollectionService (collection_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/collection/collection_controller.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_data.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_screen.dart';
import 'package:your_app/ui/fliin/screens/player/player_data.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'player_page.dart';

// The mini player shows the app's player, the one of PlayerPage: a
// track played here plays there, and a pause there shows here.
final collectionNowPlaying = CollectionNowPlayingOf(
  samplePlayer.playback,
  (playback) => switch (playback) {
    PlaybackReady(:final track, :final playing, :final buffering) => CollectionNowPlaying(
      trackId: track.id,
      title: track.title,
      artist: track.artist,
      artwork: track.artwork,
      playing: playing,
      buffering: buffering,
    ),
    _ => null,
  },
);

PlayerTrack _playerTrack(CollectionTrack track) => PlayerTrack(
  id: track.id,
  title: track.title,
  artist: track.artist,
  album: track.album,
  duration: track.duration,
  artwork: track.artwork,
);

// The album or the playlist becomes the player's queue: from the tapped
// track, from its first, or shuffled.
void playCollection(Collection collection, {CollectionTrack? from, bool shuffle = false}) => samplePlayer.playQueue(
  [for (final track in collection.tracks) _playerTrack(track)],
  from: from == null ? null : _playerTrack(from),
  shuffle: shuffle,
);

final collectionControllerProvider = Provider.autoDispose.family<CollectionController, String>((ref, collectionId) {
  final controller = CollectionController(service: SampleCollectionService(), id: collectionId)..load();
  ref.onDispose(controller.dispose);
  return controller;
});

class CollectionPage extends ConsumerWidget {
  const CollectionPage({
    super.key,
    required this.collectionId,
    required this.onBack,
  });

  final String collectionId;
  final VoidCallback onBack;

  @override
  Widget build(BuildContext context, WidgetRef ref) => CollectionScreen(
    controller: ref.watch(collectionControllerProvider(collectionId)),
    nowPlaying: collectionNowPlaying,
    onPlay: (collection, track) => playCollection(collection, from: track),
    onPlayAll: playCollection,
    onShuffle: (collection) => playCollection(collection, shuffle: true),
    onPlayPause: samplePlayer.playPause,
    onNext: samplePlayer.next,
    // The player opens out of the dock, under the finger.
    player: (context, close) => PlayerPage(onBack: close),
    onBack: onBack,
  );
}

bloc

// lib/collection_page.dart
// Your backend: implement CollectionService (collection_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/collection/collection_controller.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_data.dart';
import 'package:your_app/ui/fliin/screens/collection/collection_screen.dart';
import 'package:your_app/ui/fliin/screens/player/player_data.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

import 'player_page.dart';

// The mini player shows the app's player, the one of PlayerPage: a
// track played here plays there, and a pause there shows here.
final collectionNowPlaying = CollectionNowPlayingOf(
  samplePlayer.playback,
  (playback) => switch (playback) {
    PlaybackReady(:final track, :final playing, :final buffering) => CollectionNowPlaying(
      trackId: track.id,
      title: track.title,
      artist: track.artist,
      artwork: track.artwork,
      playing: playing,
      buffering: buffering,
    ),
    _ => null,
  },
);

PlayerTrack _playerTrack(CollectionTrack track) => PlayerTrack(
  id: track.id,
  title: track.title,
  artist: track.artist,
  album: track.album,
  duration: track.duration,
  artwork: track.artwork,
);

// The album or the playlist becomes the player's queue: from the tapped
// track, from its first, or shuffled.
void playCollection(Collection collection, {CollectionTrack? from, bool shuffle = false}) => samplePlayer.playQueue(
  [for (final track in collection.tracks) _playerTrack(track)],
  from: from == null ? null : _playerTrack(from),
  shuffle: shuffle,
);

/// The screen's controller as a bloc, for your BlocListener and BlocBuilder:
/// one emission per controller notification. `version` tells them apart
/// when `state` does not change (mode, filters, text…, read them on
/// `controller`). The screen listens to the controller itself.
class CollectionCubit extends Cubit<({CollectionState state, int version})> {
  CollectionCubit(this.controller) : super((state: controller.state, version: 0)) {
    controller.addListener(_changed);
  }

  final CollectionController controller;

  void _changed() => emit((state: controller.state, version: state.version + 1));

  @override
  Future<void> close() {
    controller
      ..removeListener(_changed)
      ..dispose();
    return super.close();
  }
}

class CollectionPage extends StatelessWidget {
  const CollectionPage({
    super.key,
    required this.collectionId,
    required this.onBack,
  });

  final String collectionId;
  final VoidCallback onBack;

  @override
  Widget build(BuildContext context) => BlocProvider(
    create: (_) => CollectionCubit(CollectionController(service: SampleCollectionService(), id: collectionId)..load()),
    child: Builder(
      builder: (context) => CollectionScreen(
        controller: context.read<CollectionCubit>().controller,
        nowPlaying: collectionNowPlaying,
        onPlay: (collection, track) => playCollection(collection, from: track),
        onPlayAll: playCollection,
        onShuffle: (collection) => playCollection(collection, shuffle: true),
        onPlayPause: samplePlayer.playPause,
        onNext: samplePlayer.next,
        // The player opens out of the dock, under the finger.
        player: (context, close) => PlayerPage(onBack: close),
        onBack: onBack,
      ),
    ),
  );
}

The snippets lead to player: install these screens too and paste their snippets.

API

CollectionScreen: class extends StatefulWidget
CollectionScreen.new: const new({super.key, required CollectionController controller, ValueListenable<CollectionNowPlaying?>? nowPlaying, void Function(Collection collection, CollectionTrack track)? onPlay, ValueChanged<Collection>? onPlayAll, ValueChanged<Collection>? onShuffle, VoidCallback? onPlayPause, VoidCallback? onNext, VoidCallback? onOpenPlayer, ValueChanged<Collection>? onMore, VoidCallback? onBack, Widget? navigation, Widget Function(BuildContext context, VoidCallback close)? player})
CollectionScreen.standalone: const new({super.key, required CollectionService service, required String id, CollectionState? initial, ValueListenable<CollectionNowPlaying?>? nowPlaying, void Function(Collection collection, CollectionTrack track)? onPlay, ValueChanged<Collection>? onPlayAll, ValueChanged<Collection>? onShuffle, VoidCallback? onPlayPause, VoidCallback? onNext, VoidCallback? onOpenPlayer, ValueChanged<Collection>? onMore, VoidCallback? onBack, Widget? navigation, Widget Function(BuildContext context, VoidCallback close)? player})
CollectionScreen.controller: get CollectionController?
CollectionScreen.service: get CollectionService?
CollectionScreen.id: get String?
CollectionScreen.initial: get CollectionState?
CollectionScreen.nowPlaying: get ValueListenable<CollectionNowPlaying?>?
CollectionScreen.onPlay: get void Function(Collection collection, CollectionTrack track)?
CollectionScreen.onPlayAll: get ValueChanged<Collection>?
CollectionScreen.onShuffle: get ValueChanged<Collection>?
CollectionScreen.onPlayPause: get VoidCallback?
CollectionScreen.onNext: get VoidCallback?
CollectionScreen.onOpenPlayer: get VoidCallback?
CollectionScreen.onMore: get ValueChanged<Collection>?
CollectionScreen.onBack: get VoidCallback?
CollectionScreen.navigation: get Widget?
CollectionScreen.player: get Widget Function(BuildContext context, VoidCallback close)?
CollectionScreen.createState: State<CollectionScreen>()
CollectionState: sealed class permits {CollectionFailed, CollectionLoading, CollectionOffline, CollectionShown}
CollectionState.new: const new()
CollectionLoading: final class extends CollectionState
CollectionLoading.new: const new()
CollectionFailed: final class extends CollectionState
CollectionFailed.new: const new()
CollectionOffline: final class extends CollectionState
CollectionOffline.new: const new()
CollectionShown: final class extends CollectionState
CollectionShown.new: const new(Collection collection)
CollectionShown.collection: get Collection
CollectionController: class extends ChangeNotifier
CollectionController.new: new({required CollectionService service, required String id, CollectionState initial})
CollectionController.service: get CollectionService
CollectionController.id: get String
CollectionController.state: get CollectionState
CollectionController.load: Future<void>()
CollectionController.dispose: void()
CollectionKind: enum {album, playlist}
CollectionKind.album: static final CollectionKind
CollectionKind.playlist: static final CollectionKind
CollectionTrack: final class
CollectionTrack.new: const new({required String id, required String title, required String artist, required String album, required Duration duration, ImageProvider? artwork})
CollectionTrack.id: get String
CollectionTrack.title: get String
CollectionTrack.artist: get String
CollectionTrack.album: get String
CollectionTrack.duration: get Duration
CollectionTrack.artwork: get ImageProvider?
Collection: final class
Collection.new: const new({required String id, required CollectionKind kind, required String title, required String byline, required List<CollectionTrack> tracks, int? year, String? description, ImageProvider? artwork})
Collection.id: get String
Collection.kind: get CollectionKind
Collection.title: get String
Collection.byline: get String
Collection.tracks: get List<CollectionTrack>
Collection.year: get int?
Collection.description: get String?
Collection.artwork: get ImageProvider?
Collection.duration: get Duration
CollectionNoNetwork: final class implements Exception
CollectionNoNetwork.new: const new()
CollectionService: abstract interface class
CollectionService.new: new()
CollectionService.collection: abstract Future<Collection>(String id)
CollectionNowPlaying: final class
CollectionNowPlaying.new: const new({required String trackId, required String title, required String artist, required bool playing, bool buffering, ImageProvider? artwork})
CollectionNowPlaying.trackId: get String
CollectionNowPlaying.title: get String
CollectionNowPlaying.artist: get String
CollectionNowPlaying.playing: get bool
CollectionNowPlaying.buffering: get bool
CollectionNowPlaying.artwork: get ImageProvider?
CollectionNowPlayingOf: final class<T> implements ValueListenable<CollectionNowPlaying?>
CollectionNowPlayingOf.new: const new(ValueListenable<T> source, CollectionNowPlaying? Function(T state) read)
CollectionNowPlayingOf.source: get ValueListenable<T>
CollectionNowPlayingOf.read: get CollectionNowPlaying? Function(T state)
CollectionNowPlayingOf.value: get CollectionNowPlaying?
CollectionNowPlayingOf.addListener: void(VoidCallback listener)
CollectionNowPlayingOf.removeListener: void(VoidCallback listener)
sampleCollectionPhotoUrls: const dynamic
sampleCollectionImage: ImageProvider(String name)
sampleCollectionDrawing: ImageProvider(String name)
sampleAlbums: List<Collection>({ImageProvider Function(String name) image})
samplePlaylists: List<Collection>({ImageProvider Function(String name) image})
sampleAlbum: Collection({ImageProvider Function(String name) image})
samplePlaylist: Collection({ImageProvider Function(String name) image})
SampleCollectionService: final class implements CollectionService
SampleCollectionService.new: new({Duration latency, ImageProvider Function(String name) image})
SampleCollectionService.latency: get Duration
SampleCollectionService.image: get ImageProvider Function(String name)
SampleCollectionService.requests: get List<String>
SampleCollectionService.collection: Future<Collection>(String id)
collectionL10n: const Map<String, Map<String, String>>
CollectionStrings: final class
CollectionStrings.new: const new(String language)
CollectionStrings.of: factory new(BuildContext context)
CollectionStrings.language: get String
CollectionStrings.digits: String(int n)
CollectionStrings.count: String(int n)
CollectionStrings.clock: String(Duration time)
CollectionStrings.spoken: String(Duration time)
CollectionStrings.length: String(Duration time)
CollectionStrings.kind: String(CollectionKind kind)
CollectionStrings.meta: String(CollectionKind kind, int year)
CollectionStrings.back: get String
CollectionStrings.more: get String
CollectionStrings.loading: get String
CollectionStrings.play: get String
CollectionStrings.shuffle: get String
CollectionStrings.nowPlaying: get String
CollectionStrings.paused: get String
CollectionStrings.openPlayer: String(CollectionNowPlaying now)
CollectionStrings.pause: get String
CollectionStrings.next: get String
CollectionStrings.expand: get String
CollectionStrings.buffering: get String
CollectionStrings.tracks: String(int count)
CollectionStrings.total: String(int count, Duration time)
CollectionStrings.emptyTitle: get String
CollectionStrings.emptyMessage: get String
CollectionStrings.errorTitle: get String
CollectionStrings.errorMessage: get String
CollectionStrings.offlineTitle: get String
CollectionStrings.offlineMessage: get String
CollectionStrings.retry: get String

Strings

Every label in English, French and Arabic. The screen follows the closest Localizations; the same table is in the manifest, ready for your ARB files.

Keyenfrar
collectionAlbumAlbumAlbumألبوم
collectionPlaylistPlaylistPlaylistقائمة تشغيل
collectionBackBackRetourرجوع
collectionMoreMorePlus d’optionsالمزيد
collectionLoadingLoading the songsChargement des morceauxجارٍ تحميل المقاطع
collectionMeta{kind} · {year}{kind} · {year}{kind} · {year}
collectionPlayPlayLireتشغيل
collectionShuffleShuffleAléatoireعشوائي
collectionNowPlayingNow playingEn cours de lectureقيد التشغيل
collectionPausedPausedEn pauseمتوقف مؤقتًا
collectionOpenPlayerNow playing: {title}, {artist}Lecture en cours : {title}, {artist}قيد التشغيل: ⁨{title}⁩، ⁨{artist}⁩
collectionOpenPlayerPausedPaused: {title}, {artist}En pause : {title}, {artist}متوقف مؤقتًا: ⁨{title}⁩، ⁨{artist}⁩
collectionPausePauseMettre en pauseإيقاف مؤقت
collectionNextNextSuivantالتالي
collectionExpandOpen the playerOuvrir le lecteurفتح المشغّل
collectionBufferingBuffering…Mise en mémoire tampon…جارٍ التخزين المؤقت…
collectionTotal{songs}, {duration}{songs}, {duration}{songs}، {duration}
collectionZero0 seconds0 seconde٠ ثانية
collectionHoursOne1 hour1 heureساعة واحدة
collectionHoursTwo2 hours2 heuresساعتان
collectionHours{count} hours{count} heures{count} ساعات
collectionHoursMany{count} hours{count} heures{count} ساعة
collectionHoursOther{count} hours{count} heures{count} ساعة
collectionMinutesOne1 minute1 minuteدقيقة واحدة
collectionMinutesTwo2 minutes2 minutesدقيقتان
collectionMinutes{count} minutes{count} minutes{count} دقائق
collectionMinutesMany{count} minutes{count} minutes{count} دقيقة
collectionMinutesOther{count} minutes{count} minutes{count} دقيقة
collectionSecondsOne1 second1 secondeثانية واحدة
collectionSecondsTwo2 seconds2 secondesثانيتان
collectionSeconds{count} seconds{count} secondes{count} ثوانٍ
collectionSecondsMany{count} seconds{count} secondes{count} ثانية
collectionSecondsOther{count} seconds{count} secondes{count} ثانية
collectionTracksOne1 song1 morceauمقطع واحد
collectionTracksTwo2 songs2 morceauxمقطعان
collectionTracks{count} songs{count} morceaux{count} مقاطع
collectionTracksMany{count} songs{count} morceaux{count} مقطعًا
collectionTracksOther{count} songs{count} morceaux{count} مقطع
collectionEmptyTitleThis playlist is emptyCette playlist est videقائمة التشغيل هذه فارغة
collectionEmptyMessageSongs you add will show here.Les morceaux que vous ajoutez s’afficheront ici.ستظهر هنا المقاطع التي تضيفها.
collectionErrorTitleThese songs could not loadImpossible de charger ces morceauxتعذّر تحميل هذه المقاطع
collectionErrorMessageTry again in a moment.Réessayez dans un instant.أعد المحاولة بعد قليل.
collectionOfflineTitleYou are offlineVous êtes hors ligneأنت غير متصل
collectionOfflineMessageConnect to see these songs.Connectez-vous pour voir ces morceaux.اتصل لعرض هذه المقاطع.
collectionRetryTry againRéessayerأعد المحاولة

Reference renders

The 16 images published with version 0.1.0, rendered by the shared harness on macOS with Flutter 3.47.7. Each shows every state side by side, at the size of an iPhone (390 × 844) or of an Android phone (412 × 915). fliin verify collection compares your copy to them.

iOS · Light
Album and playlist, iOS, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Album and playlist, iOS, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Album and playlist, iOS, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Album and playlist, iOS, Light, text 2.0×, Right to left
Text 2.0× · Right to left
iOS · Dark
Album and playlist, iOS, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Album and playlist, iOS, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Album and playlist, iOS, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Album and playlist, iOS, Dark, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Light
Album and playlist, Android, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Album and playlist, Android, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Album and playlist, Android, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Album and playlist, Android, Light, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Dark
Album and playlist, Android, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Album and playlist, Android, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Album and playlist, Android, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Album and playlist, Android, Dark, text 2.0×, Right to left
Text 2.0× · Right to left

Use it to

  • show an album with its artwork and numbered songs
  • show a playlist and the songs it holds
  • make an album play from its first song
  • make the songs of a playlist play in random order, on shuffle
  • open the page of an album or a playlist when its cover is tapped
  • show the total length and song count of an album
Not for
  • a listener's whole music: use the music library (library)
  • the song now playing: use the music player (player)
States
  • Album
  • Playlist
  • Empty
  • Loading
  • Error
  • Offline

Arrows to move, Enter to open, Escape to close.