Noir Player is a lightweight Flutter music player that demonstrates how to:
- Initialise the Android audio service with background playback support
- Query the deviceβs media library using
on_audio_query - Play, pause, stop and show the current media item in a dedicated βNow Playingβ screen
- Keep the UI responsive with
StreamBuilders
- π¦ Overview
- β¨ Features
- π Project Structure
- π Getting Started
- π§ Workflow
- π οΈ Architecture Details
- π¦ Dependencies
- π€ Contributing
- π License
Noir Player is a small, crossβplatform Flutter app that:
- Loads all local audio files from the deviceβs library.
- Shows them in a tabbed library (
All,Albums,Artists, β¦). - Initialises a background audio service (so playback keeps going while the app is backgrounded).
- Plays a selected track and navigates to a βNow Playingβ screen that displays title, artist, album art and playback controls.
| Feature | File | How it works |
|---|---|---|
| Home Screen | home_screen.dart |
Simple drawer β LibraryScreen |
| Tabβbased Library | library_screen.dart |
Uses QueryArtworkWidget & OnAudioQuery |
| Audio Service | audio_handler.dart |
Wraps audio_service & just_audio |
| Now Playing UI | player_screen.dart |
Consumes audioHandler.mediaItem & audioHandler.playbackState |
| Background playback | audio_service + just_audio |
Keeps the music playing when the user leaves the app or locks the device |
lib/
βββ main.dart
βββ core/
β βββ services/
β β βββ audio_handler.dart
β βββ theme/
β βββ app_theme.dart
βββ screens/
β βββ home/
β β βββ home_screen.dart
β βββ library/
β β βββ tabs/
β β β βββ albums_tab.dart
β β β βββ artists_tab.dart
β β β βββ playlists_tab.dart
β β β βββ songs_tab.dart
β β βββ library_screen.dart
β βββ player/
β β βββ player_screen.dart
β βββ playlists/
β β βββ playlists_screen.dart
β β βββ playlist_songs_screen.dart
β βββ albums/
β β βββ album_songs_screen.dart
β βββ artist/
β β βββ artist_songs_screen.dart
β βββ settings/
β βββ settings_screen.dart
βββ widgets/
β βββ query_artwork_widget.dart
βββ ...
Note: The
lib/core/services/audio_handler.dartfile contains the core of the audio service (initialisation, play, pause, stop, media item updates).
| Platform | Requirement |
|---|---|
| Android | Flutter SDK β₯ 2.18, Android 6.0+ |
- Make sure you have a recent version of Flutter installed:
flutter --version
- For Android youβll need the READ_EXTERNAL_STORAGE permission in
AndroidManifest.xml.
Noir Player already includes the permission request flow viaon_audio_query.
git clone https://github.com/yourβusername/noir_player.git
cd noir_player
flutter pub get# Android
flutter run -d android
> On first launch, the app will request permission to read the deviceβs music library.
> Grant the permission and the library will populate automatically.ββββββββββββββββββββ
β Noir Player UI β
β (main.dart) β
ββββββββββ¬βββββββββββ
β init
βΌ
ββββββββββββββββββββββββ
β AudioServiceManager β
β (audio_handler.dart) β
ββββββββββ¬βββββββββββββ
β start
βΌ
βββββββββββββββββββββββ
β Query Audio Files β
β (on_audio_query) β
βββββββββ¬βββββββββββββββ
β fetch list
βΌ
ββββββββββββββββββββββββ
β LibraryScreen β
β (library_screen.dart) β
βββββββββ¬ββββββββββββββββ
β tab navigation
βΌ
βββββββββββββββββββββββββ
β PlayerScreen β
β (player_screen.dart) β
βββββββββββββββββββββββββ
Each arrow represents a stream or event (e.g.,
audioHandler.mediaItem,audioHandler.playbackState).
The UI listens to these streams and updates automatically.
| # | User Action | App Reaction | Code Path |
|---|---|---|---|
| 1 | Launch app | main.dart β initAudioService() β LibraryScreen |
main.dart |
| 2 | Open drawer β tap βLibraryβ | LibraryScreen is pushed onto the Navigator stack |
home_screen.dart |
| 3 | LibraryScreen appears |
Tabs load (All / Artists / Albums). Each tab fetches tracks via on_audio_query and shows a list |
library_screen.dart |
| 4 | Tap a song | audioHandler.play(Song) is called, which: β’ Updates MediaItem stream β’ Calls JustAudio.setFilePath() β’ Starts playback |
audio_handler.dart |
| 5 | UI updates | StreamBuilder<MediaItem?> on PlayerScreen shows title/artist/album art |
player_screen.dart |
| 6 | Play/Pause button | audioHandler.play() / audioHandler.pause() toggles playback state |
player_screen.dart |
| 7 | Stop/Back button | audioHandler.stop() & Navigator.pop() |
player_screen.dart |
| 8 | Close app | Background audio continues due to audio_service configuration |
audio_handler.dart |
import 'package:flutter/material.dart';
import '../core/services/audio_handler.dart';
void main() {
runApp(const NoirPlayerApp());
}
class NoirPlayerApp extends StatelessWidget {
const NoirPlayerApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Noir Player',
theme: ThemeData.dark(),
home: const HomeScreen(),
routes: {
'/library': (_) => const LibraryScreen(),
'/player': (_) => const PlayerScreen(),
},
);
}
}
*Bootstraps the app, initialises the audio service in `main()` and defines the navigation routes.*
### `library_screen.dart`
```dart
class LibraryScreen extends StatefulWidget {
const LibraryScreen({super.key});
...
}
class _LibraryScreenState extends State<LibraryScreen> {
// TabController is used to switch between "All", "Artists", "Albums" tabs.
// Each tab uses `on_audio_query` to fetch the relevant media.
}Shows a 4βtabbed view of the local library and provides a navigation button to the βNow Playingβ screen.
import 'package:audio_service/audio_service.dart';
import 'package:just_audio/just_audio.dart';
import 'package:on_audio_query/on_audio_query.dart';
Future<void> initAudioService() async {
await AudioService.start(
backgroundTaskEntrypoint: () => AudioServiceBackground.run(() => MyAudioTask()),
androidNotificationChannelName: 'Noir Player',
androidNotificationIcon: 'mipmap/ic_launcher',
androidStopForegroundOnPause: false,
);
}Wraps the complex background audio initialization logic.
MyAudioTask extends BackgroundAudioTask (not shown in the snippet) and provides the playerStateStream, mediaItem and playback controls.
class PlayerScreen extends StatefulWidget {
const PlayerScreen({super.key});
...
}
class _PlayerScreenState extends State<PlayerScreen> {
@override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<MediaItem?>(
stream: audioHandler.mediaItem,
builder: (_, snapshot) => ...
),
bottomNavigationBar: StreamBuilder<PlaybackState>(
stream: audioHandler.playbackState,
builder: (_, snapshot) => ...
),
);
}
}Observes the global audio service streams and shows the currently playing track, its artwork, and the playback controls.
| Package | Purpose | Version |
|---|---|---|
flutter |
SDK | β₯ 2.18 |
just_audio |
Lightweight audio playback | ^0.9.27 |
audio_service |
Background audio + notification handling | ^0.18.7 |
on_audio_query |
Read deviceβs music library & artwork | ^2.5.0 |
permission_handler |
Request storage permission on Android | ^10.2.0 |
All packages are declared in
pubspec.yaml.
Runflutter pub getto install them.
Pull requests are welcome!
Please open an issue first to discuss any major changes or new features.
MIT Β© 2024 Noir Player.
See LICENSE for details.
Enjoy building your own music player with Noir Player!