Building a Music Playlist Widget with

10 Apr 2026

9K

35K

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.

Related Articles

Aug 02, 2026

Building a Flutter Notification Center with Grouping & Actions

Learn to create a sophisticated in-app notification center in Flutter, featuring grouping, swipe-to-dismiss, custom icons, and actionable buttons for enhanced u

Aug 01, 2026

Flutter Layout Tips: Using FractionallySizedBox, AspectRatio, ConstrainedBox for Adaptive UI

Master Flutter's FractionallySizedBox, AspectRatio, and ConstrainedBox to build truly adaptive UIs. Learn how to control widget sizing, maintain proportions, an

Aug 01, 2026

Flutter Product Quick View: Animation, Related Items, Badges

Learn to create a dynamic product quick view widget in Flutter, incorporating smooth animations, relevant related items, and eye-catching promo badges for an en