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.
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 collectionAlso 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.
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.
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.
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.
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.
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.
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 StringStrings
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.
| Key | en | fr | ar |
|---|---|---|---|
collectionAlbum | Album | Album | ألبوم |
collectionPlaylist | Playlist | Playlist | قائمة تشغيل |
collectionBack | Back | Retour | رجوع |
collectionMore | More | Plus d’options | المزيد |
collectionLoading | Loading the songs | Chargement des morceaux | جارٍ تحميل المقاطع |
collectionMeta | {kind} · {year} | {kind} · {year} | {kind} · {year} |
collectionPlay | Play | Lire | تشغيل |
collectionShuffle | Shuffle | Aléatoire | عشوائي |
collectionNowPlaying | Now playing | En cours de lecture | قيد التشغيل |
collectionPaused | Paused | En pause | متوقف مؤقتًا |
collectionOpenPlayer | Now playing: {title}, {artist} | Lecture en cours : {title}, {artist} | قيد التشغيل: {title}، {artist} |
collectionOpenPlayerPaused | Paused: {title}, {artist} | En pause : {title}, {artist} | متوقف مؤقتًا: {title}، {artist} |
collectionPause | Pause | Mettre en pause | إيقاف مؤقت |
collectionNext | Next | Suivant | التالي |
collectionExpand | Open the player | Ouvrir le lecteur | فتح المشغّل |
collectionBuffering | Buffering… | Mise en mémoire tampon… | جارٍ التخزين المؤقت… |
collectionTotal | {songs}, {duration} | {songs}, {duration} | {songs}، {duration} |
collectionZero | 0 seconds | 0 seconde | ٠ ثانية |
collectionHoursOne | 1 hour | 1 heure | ساعة واحدة |
collectionHoursTwo | 2 hours | 2 heures | ساعتان |
collectionHours | {count} hours | {count} heures | {count} ساعات |
collectionHoursMany | {count} hours | {count} heures | {count} ساعة |
collectionHoursOther | {count} hours | {count} heures | {count} ساعة |
collectionMinutesOne | 1 minute | 1 minute | دقيقة واحدة |
collectionMinutesTwo | 2 minutes | 2 minutes | دقيقتان |
collectionMinutes | {count} minutes | {count} minutes | {count} دقائق |
collectionMinutesMany | {count} minutes | {count} minutes | {count} دقيقة |
collectionMinutesOther | {count} minutes | {count} minutes | {count} دقيقة |
collectionSecondsOne | 1 second | 1 seconde | ثانية واحدة |
collectionSecondsTwo | 2 seconds | 2 secondes | ثانيتان |
collectionSeconds | {count} seconds | {count} secondes | {count} ثوانٍ |
collectionSecondsMany | {count} seconds | {count} secondes | {count} ثانية |
collectionSecondsOther | {count} seconds | {count} secondes | {count} ثانية |
collectionTracksOne | 1 song | 1 morceau | مقطع واحد |
collectionTracksTwo | 2 songs | 2 morceaux | مقطعان |
collectionTracks | {count} songs | {count} morceaux | {count} مقاطع |
collectionTracksMany | {count} songs | {count} morceaux | {count} مقطعًا |
collectionTracksOther | {count} songs | {count} morceaux | {count} مقطع |
collectionEmptyTitle | This playlist is empty | Cette playlist est vide | قائمة التشغيل هذه فارغة |
collectionEmptyMessage | Songs you add will show here. | Les morceaux que vous ajoutez s’afficheront ici. | ستظهر هنا المقاطع التي تضيفها. |
collectionErrorTitle | These songs could not load | Impossible de charger ces morceaux | تعذّر تحميل هذه المقاطع |
collectionErrorMessage | Try again in a moment. | Réessayez dans un instant. | أعد المحاولة بعد قليل. |
collectionOfflineTitle | You are offline | Vous êtes hors ligne | أنت غير متصل |
collectionOfflineMessage | Connect to see these songs. | Connectez-vous pour voir ces morceaux. | اتصل لعرض هذه المقاطع. |
collectionRetry | Try again | Ré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.
















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