Screens / data-display
Music player
Plays a song from the app's audio player: artwork, title and artist, a position slider read in minutes and seconds, play and pause, next and previous, shuffle, repeat and favorite.
Import
Music player 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 playerAlso installs: alert, app-bar, button, core, empty-state, mini-player, page, result-card, skeleton, slider, spinner, icons
Examples
Playing
A track playing, shuffle on. standalone holds the player over the service you pass; your audio engine implements PlayerService.
import 'package:your_app/ui/fliin/screens/player/player_data.dart';
import 'package:your_app/ui/fliin/screens/player/player_screen.dart';
import 'package:flutter/widgets.dart';
class PlayerPlayingExample extends StatelessWidget {
const PlayerPlayingExample({super.key});
@override
Widget build(BuildContext context) {
return PlayerScreen.standalone(
service: SamplePlayerService(
initial: PlaybackReady(
track: sampleTracks[0],
position: const Duration(minutes: 1, seconds: 5),
playing: true,
shuffle: true,
),
latency: const Duration(milliseconds: 900),
),
onBack: () {},
);
}
}Paused
Paused part-way, repeating the whole queue: the play button and the position say where the track stands.
import 'package:your_app/ui/fliin/screens/player/player_data.dart';
import 'package:your_app/ui/fliin/screens/player/player_screen.dart';
import 'package:flutter/widgets.dart';
class PlayerPausedExample extends StatelessWidget {
const PlayerPausedExample({super.key});
@override
Widget build(BuildContext context) {
return PlayerScreen.standalone(
service: SamplePlayerService(
initial: PlaybackReady(track: sampleTracks[1], position: const Duration(seconds: 42), repeat: PlayerRepeat.all),
latency: const Duration(milliseconds: 900),
),
onBack: () {},
);
}
}Buffering
Playing but waiting for data: buffering shows a spinner on the play button without stopping the clock.
import 'package:your_app/ui/fliin/screens/player/player_data.dart';
import 'package:your_app/ui/fliin/screens/player/player_screen.dart';
import 'package:flutter/widgets.dart';
class PlayerBufferingExample extends StatelessWidget {
const PlayerBufferingExample({super.key});
@override
Widget build(BuildContext context) {
return PlayerScreen.standalone(
service: SamplePlayerService(
initial: PlaybackReady(
track: sampleTracks[2],
position: const Duration(minutes: 12, seconds: 40),
playing: true,
buffering: true,
repeat: PlayerRepeat.one,
),
latency: const Duration(milliseconds: 900),
),
onBack: () {},
);
}
}Loading
Before the first track arrives, placeholders hold the artwork, the title and the controls.
import 'package:your_app/ui/fliin/screens/player/player_data.dart';
import 'package:your_app/ui/fliin/screens/player/player_screen.dart';
import 'package:flutter/widgets.dart';
class PlayerLoadingExample extends StatelessWidget {
const PlayerLoadingExample({super.key});
@override
Widget build(BuildContext context) {
return PlayerScreen.standalone(
service: SamplePlayerService(
initial: const PlaybackLoading(),
latency: const Duration(milliseconds: 900),
),
onBack: () {},
);
}
}Error
When a track cannot be played, the screen says so and offers to try again.
import 'package:your_app/ui/fliin/screens/player/player_data.dart';
import 'package:your_app/ui/fliin/screens/player/player_screen.dart';
import 'package:flutter/widgets.dart';
class PlayerErrorExample extends StatelessWidget {
const PlayerErrorExample({super.key});
@override
Widget build(BuildContext context) {
return PlayerScreen.standalone(
service: SamplePlayerService(
initial: PlaybackFailed(track: sampleTracks[3], position: const Duration(seconds: 20)),
latency: const Duration(milliseconds: 900),
),
onBack: () {},
);
}
}Offline
Without a connection, the screen asks to reconnect instead of showing an error.
import 'package:your_app/ui/fliin/screens/player/player_data.dart';
import 'package:your_app/ui/fliin/screens/player/player_screen.dart';
import 'package:flutter/widgets.dart';
class PlayerOfflineExample extends StatelessWidget {
const PlayerOfflineExample({super.key});
@override
Widget build(BuildContext context) {
return PlayerScreen.standalone(
service: SamplePlayerService(
initial: PlaybackFailed(track: sampleTracks[4], offline: true),
latency: const Duration(milliseconds: 900),
),
onBack: () {},
);
}
}Footer
The player with your own row under the commands: footer takes any widget, here the lyrics and what plays next.
import 'package:your_app/ui/fliin/components/button/fl_button.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:your_app/ui/fliin/screens/player/player_data.dart';
import 'package:your_app/ui/fliin/screens/player/player_screen.dart';
import 'package:flutter/widgets.dart';
class PlayerFooterExample extends StatelessWidget {
const PlayerFooterExample({super.key});
@override
Widget build(BuildContext context) {
final space = FlScope.of(context).space;
return PlayerScreen.standalone(
service: SamplePlayerService(
initial: PlaybackReady(track: sampleTracks[0], position: const Duration(minutes: 1, seconds: 5), playing: true),
latency: const Duration(milliseconds: 900),
),
onBack: () {},
onMore: () {},
footer: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: space.sm,
children: [
FlButton(label: 'Lyrics', size: FlButtonSize.sm, variant: FlButtonVariant.tertiary, onPressed: () {}),
FlButton(label: 'Up next', size: FlButtonSize.sm, variant: FlButtonVariant.tertiary, onPressed: () {}),
],
),
);
}
}Routing
fliin add player 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/player_route.dart, next to player_page.dart.
import 'package:go_router/go_router.dart';
import 'player_page.dart';
/// Add playerRoute to the routes of your GoRouter.
final playerRoute = GoRoute(
path: '/player',
builder: (context, state) => PlayerPage(
// Alone in the stack (deep link, go): your app's home.
onBack: () => context.canPop() ? context.pop() : context.go('/'),
),
);navigator
// lib/player_route.dart, next to player_page.dart.
import 'package:flutter/material.dart';
import 'player_page.dart';
/// Opens “Music player”: `Navigator.of(context).push(playerRoute())`.
Route<void> playerRoute() => MaterialPageRoute<void>(
builder: (context) => PlayerPage(
onBack: () => Navigator.of(context).maybePop(),
),
);State
The controller is a ChangeNotifier from widgets.dart, with no package. fliin add player 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/player_page.dart
// Your backend: implement PlayerService (player_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/player/player_data.dart';
import 'package:your_app/ui/fliin/screens/player/player_screen.dart';
import 'package:flutter/widgets.dart';
// The app's player lives above the routes: the library's mini player
// and this screen share its state.
final samplePlayer = SamplePlayerService();
class PlayerPage extends StatelessWidget {
const PlayerPage({
super.key,
required this.onBack,
});
final VoidCallback onBack;
@override
Widget build(BuildContext context) => PlayerScreen.standalone(
service: samplePlayer,
onBack: onBack,
);
}provider
// lib/player_page.dart
// Your backend: implement PlayerService (player_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/player/player_controller.dart';
import 'package:your_app/ui/fliin/screens/player/player_data.dart';
import 'package:your_app/ui/fliin/screens/player/player_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:provider/provider.dart';
// The app's player lives above the routes: the library's mini player
// and this screen share its state.
final samplePlayer = SamplePlayerService();
class PlayerPage extends StatelessWidget {
const PlayerPage({
super.key,
required this.onBack,
});
final VoidCallback onBack;
@override
Widget build(BuildContext context) => ChangeNotifierProvider(
create: (_) => PlayerController(service: samplePlayer),
child: Builder(
builder: (context) => PlayerScreen(
controller: context.read<PlayerController>(),
onBack: onBack,
),
),
);
}riverpod
// lib/player_page.dart
// Your backend: implement PlayerService (player_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/player/player_controller.dart';
import 'package:your_app/ui/fliin/screens/player/player_data.dart';
import 'package:your_app/ui/fliin/screens/player/player_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
// The app's player lives above the routes: the library's mini player
// and this screen share its state.
final samplePlayer = SamplePlayerService();
final playerControllerProvider = Provider.autoDispose<PlayerController>((ref) {
final controller = PlayerController(service: samplePlayer);
ref.onDispose(controller.dispose);
return controller;
});
class PlayerPage extends ConsumerWidget {
const PlayerPage({
super.key,
required this.onBack,
});
final VoidCallback onBack;
@override
Widget build(BuildContext context, WidgetRef ref) => PlayerScreen(
controller: ref.watch(playerControllerProvider),
onBack: onBack,
);
}bloc
// lib/player_page.dart
// Your backend: implement PlayerService (player_data.dart) and pass it instead of the sample service.
import 'package:your_app/ui/fliin/screens/player/player_controller.dart';
import 'package:your_app/ui/fliin/screens/player/player_data.dart';
import 'package:your_app/ui/fliin/screens/player/player_screen.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
// The app's player lives above the routes: the library's mini player
// and this screen share its state.
final samplePlayer = SamplePlayerService();
/// 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 PlayerCubit extends Cubit<({Playback state, int version})> {
PlayerCubit(this.controller) : super((state: controller.state, version: 0)) {
controller.addListener(_changed);
}
final PlayerController controller;
void _changed() => emit((state: controller.state, version: state.version + 1));
@override
Future<void> close() {
controller
..removeListener(_changed)
..dispose();
return super.close();
}
}
class PlayerPage extends StatelessWidget {
const PlayerPage({
super.key,
required this.onBack,
});
final VoidCallback onBack;
@override
Widget build(BuildContext context) => BlocProvider(
create: (_) => PlayerCubit(PlayerController(service: samplePlayer)),
child: Builder(
builder: (context) => PlayerScreen(
controller: context.read<PlayerCubit>().controller,
onBack: onBack,
),
),
);
}API
PlayerScreen: class extends StatefulWidget
PlayerScreen.new: const new({super.key, required PlayerController controller, VoidCallback? onBack, VoidCallback? onMore, Widget? footer})
PlayerScreen.standalone: const new({super.key, required PlayerService service, VoidCallback? onBack, VoidCallback? onMore, Widget? footer})
PlayerScreen.controller: get PlayerController?
PlayerScreen.service: get PlayerService?
PlayerScreen.onBack: get VoidCallback?
PlayerScreen.onMore: get VoidCallback?
PlayerScreen.footer: get Widget?
PlayerScreen.createState: State<PlayerScreen>()
PlayerNotice: enum {favoriteFailed, favoriteOffline}
PlayerNotice.favoriteFailed: static final PlayerNotice
PlayerNotice.favoriteOffline: static final PlayerNotice
PlayerController: class extends ChangeNotifier
PlayerController.new: new({required PlayerService service})
PlayerController.service: get PlayerService
PlayerController.state: get Playback
PlayerController.notice: get PlayerNotice?
PlayerController.togglePlay: void()
PlayerController.seek: Future<void>(Duration position)
PlayerController.previous: void()
PlayerController.next: void()
PlayerController.toggleShuffle: void()
PlayerController.cycleRepeat: void()
PlayerController.retry: void()
PlayerController.toggleFavorite: bool()
PlayerController.dispose: void()
PlayerTrack: final class
PlayerTrack.new: const new({required String id, required String title, required String artist, required Duration duration, String? album, ImageProvider? artwork, bool favorite})
PlayerTrack.id: get String
PlayerTrack.title: get String
PlayerTrack.artist: get String
PlayerTrack.album: get String?
PlayerTrack.duration: get Duration
PlayerTrack.artwork: get ImageProvider?
PlayerTrack.favorite: get bool
PlayerTrack.withFavorite: PlayerTrack(bool favorite)
PlayerRepeat: enum {off, all, one}
PlayerRepeat.off: static final PlayerRepeat
PlayerRepeat.all: static final PlayerRepeat
PlayerRepeat.one: static final PlayerRepeat
Playback: sealed class permits {PlaybackFailed, PlaybackLoading, PlaybackReady}
Playback.new: const new()
PlaybackLoading: final class extends Playback
PlaybackLoading.new: const new()
PlaybackReady: final class extends Playback
PlaybackReady.new: const new({required PlayerTrack track, Duration position, bool playing, bool buffering, bool shuffle, PlayerRepeat repeat, bool hasPrevious, bool hasNext})
PlaybackReady.track: get PlayerTrack
PlaybackReady.position: get Duration
PlaybackReady.playing: get bool
PlaybackReady.buffering: get bool
PlaybackReady.shuffle: get bool
PlaybackReady.repeat: get PlayerRepeat
PlaybackReady.hasPrevious: get bool
PlaybackReady.hasNext: get bool
PlaybackReady.copyWith: PlaybackReady({PlayerTrack? track, Duration? position, bool? playing, bool? buffering, bool? shuffle, PlayerRepeat? repeat, bool? hasPrevious, bool? hasNext})
PlaybackFailed: final class extends Playback
PlaybackFailed.new: const new({required PlayerTrack track, Duration position, bool offline})
PlaybackFailed.track: get PlayerTrack
PlaybackFailed.position: get Duration
PlaybackFailed.offline: get bool
PlayerNoNetwork: final class implements Exception
PlayerNoNetwork.new: const new()
PlayerService: abstract interface class
PlayerService.new: new()
PlayerService.playback: abstract get ValueListenable<Playback>
PlayerService.play: abstract void()
PlayerService.pause: abstract void()
PlayerService.seek: abstract Future<void>(Duration position)
PlayerService.previous: abstract void()
PlayerService.next: abstract void()
PlayerService.setShuffle: abstract void(bool shuffle)
PlayerService.setRepeat: abstract void(PlayerRepeat repeat)
PlayerService.retry: abstract void()
PlayerService.setFavorite: abstract Future<void>(String trackId, bool favorite)
sampleTracks: get List<PlayerTrack>
SamplePlayerService: final class implements PlayerService
SamplePlayerService.new: new({List<PlayerTrack>? queue, Playback? initial, Duration latency, bool offline, math.Random? random})
SamplePlayerService.queue: get List<PlayerTrack>
SamplePlayerService.latency: get Duration
SamplePlayerService.offline: get bool
SamplePlayerService.offline=: set bool
SamplePlayerService.favorites: get List<(String, bool)>
SamplePlayerService.seeks: get List<Duration>
SamplePlayerService.playback: get ValueListenable<Playback>
SamplePlayerService.play: void()
SamplePlayerService.pause: void()
SamplePlayerService.playPause: void()
SamplePlayerService.seek: Future<void>(Duration position)
SamplePlayerService.previous: void()
SamplePlayerService.next: void()
SamplePlayerService.setShuffle: void(bool shuffle)
SamplePlayerService.setRepeat: void(PlayerRepeat repeat)
SamplePlayerService.retry: void()
SamplePlayerService.fail: void({bool offline})
SamplePlayerService.playTrack: void(PlayerTrack track)
SamplePlayerService.setFavorite: Future<void>(String trackId, bool favorite)
playerL10n: const Map<String, Map<String, String>>
PlayerStrings: final class
PlayerStrings.new: const new(String language)
PlayerStrings.of: factory new(BuildContext context)
PlayerStrings.language: get String
PlayerStrings.digits: String(int n)
PlayerStrings.clock: String(Duration time, {required Duration total})
PlayerStrings.spoken: String(Duration time)
PlayerStrings.position: String(Duration elapsed, Duration total)
PlayerStrings.title: get String
PlayerStrings.more: get String
PlayerStrings.byline: String(String artist, String album)
PlayerStrings.remaining: String(String time)
PlayerStrings.back: get String
PlayerStrings.loading: get String
PlayerStrings.favorite: get String
PlayerStrings.favoriteChanged: String(bool favorite)
PlayerStrings.favoriteFailed: String({required bool offline})
PlayerStrings.positionLabel: get String
PlayerStrings.play: get String
PlayerStrings.pause: get String
PlayerStrings.previous: get String
PlayerStrings.next: get String
PlayerStrings.shuffle: get String
PlayerStrings.shuffleChanged: String(bool shuffle)
PlayerStrings.repeat: String(PlayerRepeat repeat)
PlayerStrings.buffering: get String
PlayerStrings.errorTitle: get String
PlayerStrings.errorMessage: get String
PlayerStrings.offlineTitle: get String
PlayerStrings.offlineMessage: get String
PlayerStrings.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 |
|---|---|---|---|
playerTitle | Now playing | Lecture en cours | قيد التشغيل |
playerBack | Back | Retour | رجوع |
playerMore | More actions | Plus d’actions | مزيد من الإجراءات |
playerByline | {artist} · {album} | {artist} · {album} | {artist} · {album} |
playerRemaining | −{time} | −{time} | −{time} |
playerLoading | Loading the track | Chargement du morceau | جارٍ تحميل المقطع |
playerFavorite | Favorite | Favori | المفضلة |
playerFavoriteAdded | Added to favorites | Ajouté aux favoris | أُضيف إلى المفضلة |
playerFavoriteRemoved | Removed from favorites | Retiré des favoris | أُزيل من المفضلة |
playerFavoriteFailed | Your favorite could not be saved. | Le favori n’a pas pu être enregistré. | تعذّر حفظ المفضلة. |
playerFavoriteOffline | You are offline. Your favorite was not saved. | Vous êtes hors ligne. Le favori n’a pas été enregistré. | أنت غير متصل. لم تُحفظ المفضلة. |
playerPosition | Position in the track | Position dans le morceau | موضع التشغيل |
playerPositionValue | {elapsed} of {total} | {elapsed} sur {total} | {elapsed} من {total} |
playerZero | 0 seconds | 0 seconde | ٠ ثانية |
playerHoursOne | 1 hour | 1 heure | ساعة واحدة |
playerHoursTwo | 2 hours | 2 heures | ساعتان |
playerHours | {count} hours | {count} heures | {count} ساعات |
playerHoursMany | {count} hours | {count} heures | {count} ساعة |
playerHoursOther | {count} hours | {count} heures | {count} ساعة |
playerMinutesOne | 1 minute | 1 minute | دقيقة واحدة |
playerMinutesTwo | 2 minutes | 2 minutes | دقيقتان |
playerMinutes | {count} minutes | {count} minutes | {count} دقائق |
playerMinutesMany | {count} minutes | {count} minutes | {count} دقيقة |
playerMinutesOther | {count} minutes | {count} minutes | {count} دقيقة |
playerSecondsOne | 1 second | 1 seconde | ثانية واحدة |
playerSecondsTwo | 2 seconds | 2 secondes | ثانيتان |
playerSeconds | {count} seconds | {count} secondes | {count} ثوانٍ |
playerSecondsMany | {count} seconds | {count} secondes | {count} ثانية |
playerSecondsOther | {count} seconds | {count} secondes | {count} ثانية |
playerPlay | Play | Lire | تشغيل |
playerPause | Pause | Mettre en pause | إيقاف مؤقت |
playerPrevious | Previous track | Morceau précédent | المقطع السابق |
playerNext | Next track | Morceau suivant | المقطع التالي |
playerShuffle | Shuffle | Lecture aléatoire | تشغيل عشوائي |
playerShuffleOn | Shuffle on | Lecture aléatoire activée | التشغيل العشوائي مفعّل |
playerShuffleOff | Shuffle off | Lecture aléatoire désactivée | التشغيل العشوائي متوقف |
playerRepeatOff | Repeat: off | Répétition : désactivée | التكرار: متوقف |
playerRepeatAll | Repeat: all tracks | Répétition : toute la file | التكرار: كل المقاطع |
playerRepeatOne | Repeat: this track | Répétition : ce morceau | التكرار: هذا المقطع |
playerBuffering | Buffering… | Mise en mémoire tampon… | جارٍ التخزين المؤقت… |
playerErrorTitle | This track could not play | Impossible de lire ce morceau | تعذّر تشغيل هذا المقطع |
playerErrorMessage | Try again in a moment. | Réessayez dans un instant. | أعد المحاولة بعد قليل. |
playerOfflineTitle | You are offline | Vous êtes hors ligne | أنت غير متصل |
playerOfflineMessage | Connect to keep listening. | Connectez-vous pour continuer l’écoute. | اتصل لمواصلة الاستماع. |
playerRetry | Try again | Réessayer | أعد المحاولة |
Reference renders
The 16 images published with version 0.2.5, 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 player compares your copy to them.
















Use it to
- toggle play and pause of the song being heard
- show the artwork, title and artist of the current song
- move to another part of the song being heard
- switch to the next or previous song
- turn on shuffle or repeat for a playlist
- add the current song to favorites
- Not for
- a list of tracks, albums or playlists: use the music library (library)
- setting a continuous value: use the slider (slider)
- States