image

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

May 14, 2026

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter

Building a Multi-Event Countdown Timer Widget with Reminders, Notifications, Repeat, and Custom Labels in Flutter Countdown timers are essential in many applic

May 11, 2026

Unleashing Dynamic UIs: Flutter's Animation Prowess

Unleashing Dynamic UIs: Flutter's Animation Prowess for Slide & Scale Effects Flutter's declarative UI framework, combined with its powerful animation capabilit

May 11, 2026

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy

Building a Product Detail Page Widget in Flutter with Related Items, Review Carousel, Promo Badges, and Quick Buy A well-designed Product Detail Page (PDP) is