Building a Music Playlist Widget with Repeat and Shuffle Options in Flutter
Creating a feature-rich music player in Flutter requires careful management of audio playback, user interface, and state. This article will guide you through building a basic music playlist widget that includes essential playback controls, along with crucial repeat and shuffle functionalities, leveraging the powerful just_audio package.
1. Setting Up Your Project
First, you need to add the just_audio dependency to your pubspec.yaml file. This package provides a robust API for playing audio files and managing playlists.
dependencies:
flutter:
sdk: flutter
just_audio: ^0.9.36 # Use the latest stable version
rxdart: ^0.27.7 # Often useful with just_audio streams, though not strictly required for basic features
After adding the dependency, run flutter pub get to fetch the packages.
2. Defining the Music Model
Let's create a simple data model for our songs. Each song will have a title and an audio URL.
class Song {
final String title;
final String url;
Song({required this.title, required this.url});
}
3. Core Player Widget and Initialization
We'll create a StatefulWidget to manage the audio player's state. In the initState, we'll initialize the AudioPlayer and load our playlist. We'll use ConcatenatingAudioSource from just_audio to manage a list of songs, which is essential for playlist features like shuffle and next/previous.
import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
import 'package:just_audio_platform_interface/just_audio_platform_interface.dart'; // Needed for LoopMode
class MusicPlaylistPlayer extends StatefulWidget {
@override
_MusicPlaylistPlayerState createState() => _MusicPlaylistPlayerState();
}
class _MusicPlaylistPlayerState extends State {
late AudioPlayer _player;
List _playlist = [
Song(title: 'Song 1', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'),
Song(title: 'Song 2', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3'),
Song(title: 'Song 3', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3'),
];
// State for repeat and shuffle options
LoopMode _loopMode = LoopMode.off;
bool _isShuffleEnabled = false;
@override
void initState() {
super.initState();
_player = AudioPlayer();
_initPlayer();
}
Future _initPlayer() async {
final _concatenatingAudioSource = ConcatenatingAudioSource(
children: _playlist
.map((song) => AudioSource.uri(Uri.parse(song.url), tag: song))
.toList(),
);
await _player.setAudioSource(_concatenatingAudioSource);
await _player.setLoopMode(_loopMode); // Set initial loop mode
await _player.setShuffleModeEnabled(_isShuffleEnabled); // Set initial shuffle mode
}
@override
void dispose() {
_player.dispose();
super.dispose();
}
// ... UI and control methods will go here
}
4. Implementing Repeat Option
The just_audio package provides setLoopMode to control repeating behavior. We can cycle through three states: `off`, `all`, and `one`.
// Inside _MusicPlaylistPlayerState
IconData _getLoopModeIcon() {
switch (_loopMode) {
case LoopMode.off:
return Icons.repeat;
case LoopMode.all:
return Icons.repeat_on;
case LoopMode.one:
return Icons.repeat_one_on;
}
}
String _getLoopModeTooltip() {
switch (_loopMode) {
case LoopMode.off:
return 'Repeat Off';
case LoopMode.all:
return 'Repeat All';
case LoopMode.one:
return 'Repeat One';
}
}
void _toggleLoopMode() {
setState(() {
if (_loopMode == LoopMode.off) {
_loopMode = LoopMode.all;
} else if (_loopMode == LoopMode.all) {
_loopMode = LoopMode.one;
} else {
_loopMode = LoopMode.off;
}
_player.setLoopMode(_loopMode);
});
}
5. Implementing Shuffle Option
Enabling shuffle mode in just_audio is straightforward with setShuffleModeEnabled and `shuffle()`. When shuffle is enabled, `just_audio` will play the tracks in a random order. You can use `shuffle()` to re-shuffle the playlist at any time.
// Inside _MusicPlaylistPlayerState
void _toggleShuffle() {
setState(() {
_isShuffleEnabled = !_isShuffleEnabled;
_player.setShuffleModeEnabled(_isShuffleEnabled);
if (_isShuffleEnabled) {
_player.shuffle(); // Shuffle the current playlist order
}
});
}
6. Building the User Interface (UI) and Controls
Now, let's assemble the UI, including buttons for play/pause, next/previous, repeat, and shuffle. We'll use StreamBuilder to react to changes in the player's state.
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Music Playlist Player')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Display current song title
StreamBuilder(
stream: _player.sequenceStateStream,
builder: (context, snapshot) {
final state = snapshot.data;
if (state == null || state.currentSource == null) {
return Text('No song playing');
}
final song = state.currentSource!.tag as Song;
return Text(
song.title,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
);
},
),
SizedBox(height: 20),
// Playback controls
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Previous button
IconButton(
icon: Icon(Icons.skip_previous),
iconSize: 48.0,
onPressed: _player.hasPrevious ? _player.seekToPrevious : null,
),
// Play/Pause button
StreamBuilder(
stream: _player.playerStateStream,
builder: (context, snapshot) {
final playerState = snapshot.data;
final processingState = playerState?.processingState;
final playing = playerState?.playing;
if (processingState == ProcessingState.loading ||
processingState == ProcessingState.buffering) {
return Container(
margin: EdgeInsets.all(8.0),
width: 64.0,
height: 64.0,
child: CircularProgressIndicator(),
);
} else if (playing != true) {
return IconButton(
icon: Icon(Icons.play_arrow),
iconSize: 64.0,
onPressed: _player.play,
);
} else if (processingState != ProcessingState.completed) {
return IconButton(
icon: Icon(Icons.pause),
iconSize: 64.0,
onPressed: _player.pause,
);
} else {
return IconButton(
icon: Icon(Icons.replay),
iconSize: 64.0,
onPressed: () => _player.seek(Duration.zero, index: 0),
);
}
},
),
// Next button
IconButton(
icon: Icon(Icons.skip_next),
iconSize: 48.0,
onPressed: _player.hasNext ? _player.seekToNext : null,
),
],
),
SizedBox(height: 20),
// Repeat and Shuffle buttons
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Repeat button
IconButton(
icon: Icon(_getLoopModeIcon()),
tooltip: _getLoopModeTooltip(),
iconSize: 32.0,
onPressed: _toggleLoopMode,
),
SizedBox(width: 20),
// Shuffle button
IconButton(
icon: Icon(
Icons.shuffle,
color: _isShuffleEnabled ? Colors.blue : Colors.grey,
),
tooltip: 'Shuffle',
iconSize: 32.0,
onPressed: _toggleShuffle,
),
],
),
],
),
),
);
}
7. Full Example Code (for context)
Here's how the complete MusicPlaylistPlayer widget would look with all the pieces combined.
import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
import 'package:just_audio_platform_interface/just_audio_platform_interface.dart';
class Song {
final String title;
final String url;
Song({required this.title, required this.url});
}
class MusicPlaylistPlayer extends StatefulWidget {
@override
_MusicPlaylistPlayerState createState() => _MusicPlaylistPlayerState();
}
class _MusicPlaylistPlayerState extends State {
late AudioPlayer _player;
List _playlist = [
Song(title: 'Song 1', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'),
Song(title: 'Song 2', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3'),
Song(title: 'Song 3', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3'),
Song(title: 'Song 4', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-3.mp3'), // Add more songs for better shuffle demonstration
Song(title: 'Song 5', url: 'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-2.mp3'),
];
LoopMode _loopMode = LoopMode.off;
bool _isShuffleEnabled = false;
@override
void initState() {
super.initState();
_player = AudioPlayer();
_initPlayer();
}
Future _initPlayer() async {
final _concatenatingAudioSource = ConcatenatingAudioSource(
shuffleModeEnabled: _isShuffleEnabled, // Initial shuffle state
children: _playlist
.map((song) => AudioSource.uri(Uri.parse(song.url), tag: song))
.toList(),
);
await _player.setAudioSource(_concatenatingAudioSource);
await _player.setLoopMode(_loopMode);
}
@override
void dispose() {
_player.dispose();
super.dispose();
}
IconData _getLoopModeIcon() {
switch (_loopMode) {
case LoopMode.off:
return Icons.repeat;
case LoopMode.all:
return Icons.repeat_on;
case LoopMode.one:
return Icons.repeat_one_on;
}
}
String _getLoopModeTooltip() {
switch (_loopMode) {
case LoopMode.off:
return 'Repeat Off';
case LoopMode.all:
return 'Repeat All';
case LoopMode.one:
return 'Repeat One';
}
}
void _toggleLoopMode() {
setState(() {
if (_loopMode == LoopMode.off) {
_loopMode = LoopMode.all;
} else if (_loopMode == LoopMode.all) {
_loopMode = LoopMode.one;
} else {
_loopMode = LoopMode.off;
}
_player.setLoopMode(_loopMode);
});
}
void _toggleShuffle() {
setState(() {
_isShuffleEnabled = !_isShuffleEnabled;
_player.setShuffleModeEnabled(_isShuffleEnabled);
if (_isShuffleEnabled) {
_player.shuffle(); // Reshuffle the playlist when enabled
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Music Playlist Player')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
StreamBuilder(
stream: _player.sequenceStateStream,
builder: (context, snapshot) {
final state = snapshot.data;
if (state == null || state.currentSource == null) {
return Text('No song playing');
}
final song = state.currentSource!.tag as Song;
return Text(
song.title,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
);
},
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(Icons.skip_previous),
iconSize: 48.0,
onPressed: _player.hasPrevious ? _player.seekToPrevious : null,
),
StreamBuilder(
stream: _player.playerStateStream,
builder: (context, snapshot) {
final playerState = snapshot.data;
final processingState = playerState?.processingState;
final playing = playerState?.playing;
if (processingState == ProcessingState.loading ||
processingState == ProcessingState.buffering) {
return Container(
margin: EdgeInsets.all(8.0),
width: 64.0,
height: 64.0,
child: CircularProgressIndicator(),
);
} else if (playing != true) {
return IconButton(
icon: Icon(Icons.play_arrow),
iconSize: 64.0,
onPressed: _player.play,
);
} else if (processingState != ProcessingState.completed) {
return IconButton(
icon: Icon(Icons.pause),
iconSize: 64.0,
onPressed: _player.pause,
);
} else {
return IconButton(
icon: Icon(Icons.replay),
iconSize: 64.0,
onPressed: () => _player.seek(Duration.zero, index: 0),
);
}
},
),
IconButton(
icon: Icon(Icons.skip_next),
iconSize: 48.0,
onPressed: _player.hasNext ? _player.seekToNext : null,
),
],
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(_getLoopModeIcon()),
tooltip: _getLoopModeTooltip(),
iconSize: 32.0,
onPressed: _toggleLoopMode,
),
SizedBox(width: 20),
IconButton(
icon: Icon(
Icons.shuffle,
color: _isShuffleEnabled ? Colors.blue : Colors.grey,
),
tooltip: 'Shuffle',
iconSize: 32.0,
onPressed: _toggleShuffle,
),
],
),
],
),
),
);
}
}
Conclusion
You've successfully built a foundational music playlist widget in Flutter with repeat and shuffle options using the just_audio package. This setup provides a robust base for more advanced features like progress indicators, volume controls, and background audio playback. By managing the LoopMode and shuffleModeEnabled properties of AudioPlayer, you can offer a flexible and user-friendly experience for your music application.