Managing playlists in Flutter with Just Audio

Suragch
Suragch
May 2 · 12 min read

Loading, looping, seeking, shuffling, and editing

This tutorial will teach you how to play and navigate multiple songs using the Just Audio plugin for Flutter. You’ll learn about loading audio sources, seeking to another song, repeating a song or playlist, randomizing the song order, and editing the contents of the playlist. If you’re new to Just Audio, you should go through the previous tutorials first since this one will assume you’re already familiar with the basics:

  1. Playing short audio clips in Flutter with Just Audio
  2. Streaming audio in Flutter with Just Audio

Setup

Rather than walking you through the entire UI and state management setup process again, this time I’ll provide you with a starter project. The starter project begins where the last tutorial left off, that is, with streaming a single audio file from a URL.

Download or clone the GitHub repository for the project:

git clone https://github.com/suragch/audio_playlist_flutter_demo.git

Then open the starter folder in your IDE.

Run the app to see where the starter project begins.

Starter app

This will play a single song when you press the play button just like you did in the Just Audio steaming tutorial. None of the other buttons work yet, though. I’ve added them to the UI, but they’re missing the logic to make them do anything. That’s your job. By the end of the tutorial you’ve have them all working well.

Go ahead and browse the UI widgets in main.dart now. You’ll see that I wrapped some widgets with a ValueListenableBuilder. These widgets will rebuild when there’s a state change. This is a simple state management solution I’ve described previously in Flutter state management for minimalists. You’re welcome to switch it out with Provider or Bloc if you prefer.

Go to pubspec.yaml and you’ll see that you’re using just_audio for the audio player and audio_video_progress_bar for the progress bar just like before. Nothing has changed here.

dependencies:
just_audio: ^0.7.4
audio_video_progress_bar: ^0.4.0

The work you’re going to do in this tutorial is filling in the audio player logic in the page_manager.dart state management file. Open that file now. Note that PageManager starts with a list of value notifiers:

final currentSongTitleNotifier = ValueNotifier<String>('');
final playlistNotifier = ValueNotifier<List<String>>([]);
final progressNotifier = ProgressNotifier();
final repeatButtonNotifier = RepeatButtonNotifier();
final isFirstSongNotifier = ValueNotifier<bool>(true);
final playButtonNotifier = PlayButtonNotifier();
final isLastSongNotifier = ValueNotifier<bool>(true);
final isShuffleModeEnabledNotifier = ValueNotifier<bool>(false);

Each of these are for updating a specific part of the UI (and thus pair with a ValueListenableBuilder there) when certain audio events happen.

You already implemented progressNotifier and playButtonNotifier last time. I’ve only made very minor updates to your code by extracting those notifiers into their own classes and also extracting the audio stream listeners into smaller methods.

If you browse the rest of page_manager.dart you’ll see a lot of TODOs. These are what you’ll work on for the remainder of the tutorial.

Loading a playlist

Last time you streamed one song, but this time you want to play an entire playlist. The way to do that in Just Audio is by creating a ConcatenatingAudioSource object and adding audio source items to it.

In page_manager.dart add the following field to PageManager:

late ConcatenatingAudioSource _playlist;

Note: Anytime you get lost, you can consult the final project.

One benefit of using ConcatenatingAudioSource is that it allows gapless playback for the media items it contains. That means you don’t need to wait for the next song to load when the previous song finishes. The playlist will prefetch it.

Now find the _setInitialPlaylist method and replace it with with the following code:

void _setInitialPlaylist() async {
const prefix = 'https://www.soundhelix.com/examples/mp3';
final song1 = Uri.parse('$prefix/SoundHelix-Song-1.mp3');
final song2 = Uri.parse('$prefix/SoundHelix-Song-2.mp3');
final song3 = Uri.parse('$prefix/SoundHelix-Song-3.mp3');
_playlist = ConcatenatingAudioSource(children: [
AudioSource.uri(song1, tag: 'Song 1'),
AudioSource.uri(song2, tag: 'Song 2'),
AudioSource.uri(song3, tag: 'Song 3'),
]);
await _audioPlayer.setAudioSource(_playlist);
}

Notes:

  • A playlist can contain a file from assets, the user’s device, or a web link. It can even contain a DASH or HLS stream. In this tutorial, though, you’ll just use some freely available mp3 audio files streamed from SoundHelix, which you added to the playlist by using AudioSource.uri.
  • The optional tag parameter is dynamic so you can give it anything. In this case you simply assign it a string with the song’s title. Check out the official Just Audio example project to see how they used a custom class here called AudioMetadata to store the album, title, and artwork URL.

You have three songs for now, but you’ll add more later.

Run the app. When one song finishes (or you move the slider to the end of the track), the next song will automatically start to play.

That’s progress. It would be nice to have the UI update based on the current song that’s playing, though. Let’s work on that next.

Updating the UI for audio events

Just Audio has many different event streams that you can listen to. Here are five that are related to the tasks we need to accomplish today:

  • sequenceStream: This is your playlist in the order you added them. Specifically, it’s a list of IndexedAudioSource. Remember the tag items you added earlier? They’re all inside the audio source objects in this list. This stream will yield a new list whenever the playlist changes.
  • shuffleModeEnabledStream: Shuffle mode means that you can play the songs in a random order. You can turn this mode on and off, and when you do, this stream will yield a true or false.
  • shuffleIndicesStream: This is a shuffled list of integers pointing to items in the playlist from sequenceStream (which itself doesn’t get shuffled). This stream will yield a new list when you call the shuffle method on the audio player.
  • currentIndexStream: This stream notifies you of the index of the current song when it first begins. This integer points to an audio source in the playlist from sequenceStream.
  • loopModeStream: The default is to play all of the songs in the playlist once. However, you can also choose to repeat a single song or even repeat the playlist. Just Audio calls this the loop mode and this stream will yield a new value of type LoopMode every time the loop mode changes.

It would be possible to listen to each of these streams individually, but Just Audio conveniently combines them into a single stream called sequenceStateStream. Any time one of the five streams above yields a new value, sequenceStateStream produces a combined value of type SequenceState. Here is an shortened version of the SequenceState data class:

class SequenceState {
final List<IndexedAudioSource> sequence;
final int currentIndex;
final List<int> shuffleIndices;
final bool shuffleModeEnabled;
final LoopMode loopMode;
IndexedAudioSource? get currentSource => ...
List<IndexedAudioSource> get effectiveSequence => ...
}

currentSource and effectiveSequence are convenience getters based on the other values. I’ll explain them more a little later.

Note: Combining all of those streams into one will result in a slight bit of unncessary rebuilding for your UI. For example, the shuffle button doesn’t actually need to rebuild when the current song index changes. However, this extra rebuilding very minimal since none of the original streams update very often. In my opinion the convenience makes it worth it. But feel free to listen to the individual streams listed above rather than sequenceStateStream if you prefer.

Find the method named _listenForChangesInSequenceState in page_manager.dart and replace it with the following code:

void _listenForChangesInSequenceState() {
_audioPlayer.sequenceStateStream.listen((sequenceState) {
if (sequenceState == null) return;
// TODO: update current song title
// TODO: update playlist
// TODO: update shuffle mode
// TODO: update previous and next buttons
});
}

You’re now listening for changes in SequenceState. You’ll complete each of the TODOs in the following sections.

After the line // TODO: update current song title, add the following code:

final currentItem = sequenceState.currentSource;
final title = currentItem?.tag as String?;
currentSongTitleNotifier.value = title ?? '';

The current item is the audio source that’s currently playing. Since that audio source contains the title in tag, you can use that to update the UI.

Run the app to see the difference. Now the song title appears at the top.

If you play to the next song you’ll see the title change.

After the line // TODO: update playlist, add the following code:

final playlist = sequenceState.effectiveSequence;
final titles = playlist.map((item) => item.tag as String).toList();
playlistNotifier.value = titles;

Notes:

  • The effectiveSequence takes into account both the indexed playlist and whether or not shuffle mode is on, and if so, what the shuffled order is. You’re not shuffling yet, but you will later.
  • After you have the list, you get the song titles from each audio source tag.
  • Finally, you notify the UI to update with the new list of song titles.

Run the app again to see the list:

Since you’re not shuffling the list or adding or removing any items yet, you won’t see any change when you go to the next song.

After the line // TODO: update shuffle mode, add the following code:

isShuffleModeEnabledNotifier.value = sequenceState.shuffleModeEnabled;

This simply tells the UI to update based on the current value of shuffle mode.

Since you haven’t implemented shuffle mode yet, there’s no need to rerun the app. However, this is the part of the UI that it will effect:

After the line // TODO: update previous and next buttons, add the following code:

if (playlist.isEmpty || currentItem == null) {
isFirstSongNotifier.value = true;
isLastSongNotifier.value = true;
} else {
isFirstSongNotifier.value = playlist.first == currentItem;
isLastSongNotifier.value = playlist.last == currentItem;
}

If you’re at the first song, you shouldn’t be able to go to the previous song. Similarly, if you’re at the last song you shouldn’t be able to go to the next song. By checking whether currentItem is first or last you can disable the UI buttons appropriately.

Run the app and manually move through each song by moving the progress bar thumb to the end of the song (since you haven’t implemented the button logic needed to seek to the next song yet).

Notice how the seek button colors update depending on the current song:

Grey means disabled and black means enabled. Of course, even when the buttons are enabled, they still don’t do anything when you press them. That will come later.

The UI notifications are finished. Next you need to tell the audio player what to do when the users press the various buttons.

Implementing the audio control buttons

We haven’t done anything with repeat button, so let’s handle that one first.

Find the onRepeatButtonPressed method and replace it with the following code:

void onRepeatButtonPressed() {
repeatButtonNotifier.nextState();
switch (repeatButtonNotifier.value) {
case RepeatState.off:
_audioPlayer.setLoopMode(LoopMode.off);
break;
case RepeatState.repeatSong:
_audioPlayer.setLoopMode(LoopMode.one);
break;
case RepeatState.repeatPlaylist:
_audioPlayer.setLoopMode(LoopMode.all);
}
}

First you updated the notifier, where nextState is already implemented in the starter project and just loops through each of the enum values. Then you directly convert the RepeatState enum (defined by this app) to the LoopMode enum (defined by Just Audio) and use that to set the loop mode on the audio player.

“Why did you do that?” you ask. “Why not just use LoopMode for everything? Why did you create a whole new enum called RepeatState when it means exactly the same thing as LoopMode” The reason is that I’m trying to completely isolate Just Audio from the UI. I want to keep it exclusively in the state management class. As much as I like Just Audio, if it goes under some day, I don’t want to have to pick it out piece by piece from all across my app. By isolating a third-party plugin to one location in your app, you limit your pain if and when you need to switch to a different plugin.

Run the app to observe the new behavior. When you click the repeat button the icon changes. Also, if you let the song or playlist play to the end, it will start over at the beginning of the song or playlist if you have that particular mode selected.

repeat off (left), repeat song (middle), repeat playlist (right)

Find the onPreviousSongButtonPressed method and replace it with the following code:

void onPreviousSongButtonPressed() {
_audioPlayer.seekToPrevious();
}

This tells the audio player to set the audio source to the index before the current one.

The process for seeking to the next song is pretty much the same. Replace onNextSongButtonPressed with the following:

void onNextSongButtonPressed() {
_audioPlayer.seekToNext();
}

Run the app and press the next song and previous song buttons.

Demonstrating the previous and next buttons

Just Audio will allow you to randomize the play order. This is called shuffling. To do that, find the onShuffleButtonPressed method and replace it with the following code:

void onShuffleButtonPressed() async {
final enable = !_audioPlayer.shuffleModeEnabled;
if (enable) {
await _audioPlayer.shuffle();
}
await _audioPlayer.setShuffleModeEnabled(enable);
}

First you check if shuffle mode is already enabled, and if not, then you shuffle the songs and enable shuffle mode. Otherwise you turn shuffle mode off.

Run the app and press the shuffle button. Pay attention to the playlist order when shuffle mode is on. If you press the next button, you’ll go the the next shuffled song. For example, you’ll go from Song 1 to Song 3 (if Song 3 is the next shuffled song after Song 1).

Shuffling in action

That finishes up the main audio control buttons, but you still need to implement the logic for adding and removing items from the playlist.

Editing playlists

The current playlist has three items. However, it’s possible to add and remove songs from the playlist — even while the audio is playing.

The UI is already set up to demonstrate these actions by giving you add and remove buttons:

You just need to implement the logic on the state management side.

Note: Having two big blue buttons for this job isn’t a great UX choice, but it does work nicely for making a simple tutorial. See the official Just Audio example app for a more sophisticated way to remove items from the playlist.

Find the addSong method and replace it with the following code:

void addSong() {
final songNumber = _playlist.length + 1;
const prefix = 'https://www.soundhelix.com/examples/mp3';
final song = Uri.parse('$prefix/SoundHelix-Song-$songNumber.mp3');
_playlist.add(AudioSource.uri(song, tag: 'Song $songNumber'));
}

The SoundHelix URLs you’re using for the songs are all identical except for the song number, so that makes it convenient to generate a new URL. Once you have the URL, you call _playlist.add to add the new audio source to the playlist.

Note: There are only 16 example songs on SoundHelix, but this method will allow you to keep generating even more URLs and adding them to the playlist. This tutorial isn’t covering error handling, but the Just Audio audio player will throw errors, so you should look into what the possible errors could be and how you want to handle them when they happen. Check the just_audio documentation, example project, and source code for help with this.

Find the removeSong method and replace it with the following code:

void removeSong() {
final index = _playlist.length - 1;
if (index < 0) return;
_playlist.removeAt(index);
}

While removeAt can remove a song at any index in the playlist, this particular implementation removes the final song.

This tutorial doesn’t use them, but there are a number of other ways you can edit the playlist:

  • addAll: Adds a list of audio sources to the end of the playlist.
  • removeRange: Removes multiple audio source items from the playlist based on a start and end index range.
  • insert: Adds an audio source at a specified index location in the playlist.
  • insertAll: Adds a list of audio sources at a specified index location in the playlist.
  • move: Moves an audio source from one index location in the playlist to another.
  • clear: Removes all audio source items from the playlist.

To see move in action, check out the official example app for Just Audio.

Testing it all out

You’re all finished! Run the app one more time to make sure everything works.

If you had any difficulties, be sure to check out the final project source code.

Going on

As far as you’ve come, there’s still more to do. If your app has a playlist, your mobile users will almost certainly want to be able to control the audio from the lock screen or control panel. To do that you’ll need to wrap the audio player with audio_service, another package made by Ryan Heise, the author of Just Audio. I’d like to write a tutorial for that, too, but in the mean time you can check out the documentation.

Follow Flutter Community on Twitter: https://www.twitter.com/FlutterComm

Flutter Community

Articles and Stories from the Flutter Community

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store