Skip to main content
RapidDev - Software Development Agency
flutterflow-tutorials

How to create a custom audio player for your FlutterFlow app

Build a fully functional audio player in FlutterFlow using a Custom Widget with the just_audio package. You will create play/pause/seek controls, a live progress bar powered by StreamBuilder on positionStream, duration display, and playlist support with ConcatenatingAudioSource. The key challenge is managing three player states (loading, playing, paused) and disposing the player to prevent background audio leaks.

What you'll learn

  • How to create a Custom Widget with just_audio for audio playback
  • How to build a live progress bar using StreamBuilder on positionStream
  • How to manage play/pause/loading states with playerStateStream
  • How to properly dispose the audio player to prevent background playback
Book a free consultation
4.9Clutch rating
600+Happy partners
17+Countries served
190+Team members
Beginner7 min read20-30 minFlutterFlow Pro+ (Custom Code required)March 2026RapidDev Engineering Team
TL;DR

Build a fully functional audio player in FlutterFlow using a Custom Widget with the just_audio package. You will create play/pause/seek controls, a live progress bar powered by StreamBuilder on positionStream, duration display, and playlist support with ConcatenatingAudioSource. The key challenge is managing three player states (loading, playing, paused) and disposing the player to prevent background audio leaks.

Building an audio player Custom Widget with just_audio

Audio playback is a common requirement for podcast apps, music players, guided meditation, and language learning apps. FlutterFlow has no built-in audio player widget, so you need a Custom Widget using the just_audio package. This tutorial builds a complete player with play/pause, seek bar, time display, and proper lifecycle handling.

Prerequisites

  • FlutterFlow Pro plan or higher (Custom Code required)
  • A direct URL to an audio file (MP3, AAC, or WAV) or a Firebase Storage audio URL
  • Basic understanding of FlutterFlow Custom Widgets
  • A FlutterFlow project open in the builder

Step-by-step guide

1

Add the just_audio dependency

Go to Custom Code → Pubspec Dependencies. Add just_audio version ^0.9.36. This package provides AudioPlayer with streaming support, background audio, and platform-specific audio session handling for both iOS and Android. It is the most widely used audio package in the Flutter ecosystem with active maintenance.

Expected result: just_audio appears in dependencies without version conflicts.

2

Create the Custom Widget with AudioPlayer controller

Create Custom Widget named CustomAudioPlayer. Add a required parameter audioUrl (String). In initState(), create an AudioPlayer instance and call player.setUrl(widget.audioUrl). Do NOT call player.play() in initState — let the user tap play. In dispose(), call player.dispose() to stop playback and release resources. This prevents the common bug where audio continues playing after the user navigates away from the page.

custom_audio_player.dart
1import 'package:just_audio/just_audio.dart';
2
3class CustomAudioPlayer extends StatefulWidget {
4 const CustomAudioPlayer({super.key, this.width, this.height, required this.audioUrl});
5 final double? width;
6 final double? height;
7 final String audioUrl;
8 @override
9 State<CustomAudioPlayer> createState() => _CustomAudioPlayerState();
10}
11
12class _CustomAudioPlayerState extends State<CustomAudioPlayer> {
13 final AudioPlayer _player = AudioPlayer();
14
15 @override
16 void initState() {
17 super.initState();
18 _player.setUrl(widget.audioUrl);
19 }
20
21 @override
22 void dispose() {
23 _player.dispose();
24 super.dispose();
25 }

Expected result: Widget compiles. Player loads the audio URL on initialization.

3

Build the player UI with StreamBuilder for live updates

In build(), use three StreamBuilders for reactive UI: (1) StreamBuilder<PlayerState> on player.playerStateStream — shows play icon when paused/completed, pause icon when playing, spinner when loading/buffering. (2) StreamBuilder<Duration?> on player.durationStream — displays total track duration. (3) StreamBuilder<Duration> on player.positionStream — updates the Slider position and current time text. The Slider's onChanged calls player.seek(Duration(milliseconds: value)).

custom_audio_player.dart (build method)
1StreamBuilder<PlayerState>(
2 stream: _player.playerStateStream,
3 builder: (context, snapshot) {
4 final state = snapshot.data;
5 final playing = state?.playing ?? false;
6 final processingState = state?.processingState;
7 if (processingState == ProcessingState.loading ||
8 processingState == ProcessingState.buffering) {
9 return const SizedBox(width: 48, height: 48, child: CircularProgressIndicator());
10 }
11 return IconButton(
12 iconSize: 48,
13 icon: Icon(playing ? Icons.pause_circle_filled : Icons.play_circle_filled),
14 onPressed: playing ? _player.pause : _player.play,
15 );
16 },
17),

Expected result: Player shows play/pause/loading states reactively. Slider tracks audio position in real time.

4

Add skip forward/backward and duration display

Add two IconButtons for skip: one calls player.seek(player.position + Duration(seconds: 15)) for skip-forward, another subtracts 15 seconds for skip-backward. For time display, format Duration to mm:ss using a helper: '${d.inMinutes.remainder(60).toString().padLeft(2,'0')}:${d.inSeconds.remainder(60).toString().padLeft(2,'0')}'. Show current position on the left and total duration on the right of the Slider.

Expected result: Users can skip ±15 seconds and see formatted time labels (e.g., 02:34 / 05:12).

5

Pass the audio URL as a Component Parameter for reuse

The audioUrl String parameter lets you reuse this widget across your app — podcast detail page, meditation player, language lesson, etc. When placing the widget on a page, bind audioUrl to a Page State variable, Route Parameter, or Backend Query result field (e.g., lesson.audioFileUrl from Firestore). For playlists, extend the widget to accept a list of URLs and use ConcatenatingAudioSource.

Expected result: Widget displays and plays different audio files depending on the URL passed from the parent page.

Complete working example

custom_audio_player.dart
1import 'package:just_audio/just_audio.dart';
2
3class CustomAudioPlayer extends StatefulWidget {
4 const CustomAudioPlayer({super.key, this.width, this.height, required this.audioUrl});
5 final double? width;
6 final double? height;
7 final String audioUrl;
8 @override
9 State<CustomAudioPlayer> createState() => _CustomAudioPlayerState();
10}
11
12class _CustomAudioPlayerState extends State<CustomAudioPlayer> {
13 final AudioPlayer _player = AudioPlayer();
14
15 @override
16 void initState() {
17 super.initState();
18 _player.setUrl(widget.audioUrl);
19 }
20
21 @override
22 void dispose() {
23 _player.dispose();
24 super.dispose();
25 }
26
27 String _fmt(Duration d) =>
28 '${d.inMinutes.remainder(60).toString().padLeft(2, '0')}'
29 ':${d.inSeconds.remainder(60).toString().padLeft(2, '0')}';
30
31 @override
32 Widget build(BuildContext context) {
33 return SizedBox(
34 width: widget.width ?? double.infinity,
35 child: Column(mainAxisSize: MainAxisSize.min, children: [
36 // Play/Pause + Skip buttons
37 Row(mainAxisAlignment: MainAxisAlignment.center, children: [
38 IconButton(
39 icon: const Icon(Icons.replay_10),
40 onPressed: () => _player.seek(
41 _player.position - const Duration(seconds: 10)),
42 ),
43 StreamBuilder<PlayerState>(
44 stream: _player.playerStateStream,
45 builder: (ctx, snap) {
46 final state = snap.data;
47 final processing = state?.processingState;
48 if (processing == ProcessingState.loading ||
49 processing == ProcessingState.buffering) {
50 return const SizedBox(
51 width: 48, height: 48,
52 child: CircularProgressIndicator());
53 }
54 final playing = state?.playing ?? false;
55 return IconButton(
56 iconSize: 48,
57 icon: Icon(playing
58 ? Icons.pause_circle_filled
59 : Icons.play_circle_filled),
60 onPressed: playing ? _player.pause : _player.play,
61 );
62 },
63 ),
64 IconButton(
65 icon: const Icon(Icons.forward_30),
66 onPressed: () => _player.seek(
67 _player.position + const Duration(seconds: 30)),
68 ),
69 ]),
70 // Seek bar + time labels
71 StreamBuilder<Duration?>(stream: _player.durationStream,
72 builder: (ctx, durSnap) {
73 final duration = durSnap.data ?? Duration.zero;
74 return StreamBuilder<Duration>(
75 stream: _player.positionStream,
76 builder: (ctx, posSnap) {
77 final position = posSnap.data ?? Duration.zero;
78 return Column(children: [
79 Slider(
80 min: 0,
81 max: duration.inMilliseconds.toDouble().clamp(1, double.infinity),
82 value: position.inMilliseconds.toDouble().clamp(0, duration.inMilliseconds.toDouble()),
83 onChanged: (v) => _player.seek(Duration(milliseconds: v.toInt())),
84 ),
85 Padding(
86 padding: const EdgeInsets.symmetric(horizontal: 16),
87 child: Row(
88 mainAxisAlignment: MainAxisAlignment.spaceBetween,
89 children: [Text(_fmt(position)), Text(_fmt(duration))],
90 ),
91 ),
92 ]);
93 },
94 );
95 },
96 ),
97 ]),
98 );
99 }
100}

Common mistakes when creating a custom audio player for your FlutterFlow app

Why it's a problem: Not calling player.dispose() in the widget's dispose method

How to avoid: Always override dispose() and call _player.dispose() before super.dispose(). This stops playback and releases all resources.

Why it's a problem: Calling player.play() in initState

How to avoid: Only load the URL in initState with setUrl(). Let the user explicitly tap the play button to start playback.

Why it's a problem: Not handling buffering state — showing play button during load

How to avoid: Use StreamBuilder on playerStateStream to show a CircularProgressIndicator during ProcessingState.loading and ProcessingState.buffering states.

Best practices

  • Always dispose the AudioPlayer in the widget's dispose() method
  • Show loading/buffering state explicitly with a spinner — never leave the UI ambiguous
  • Format duration as mm:ss using padLeft(2, '0') for consistent display
  • Pass audio URL as Component Parameter for reusability across pages
  • Use ConcatenatingAudioSource for playlists instead of manually managing multiple players
  • Clamp Slider value to prevent RangeError when position exceeds duration during seek
  • Test with slow network — audio buffering is the most common real-world UX issue

Still stuck?

Copy one of these prompts to get a personalized, step-by-step explanation.

ChatGPT Prompt

Write a Flutter Custom Widget for FlutterFlow that plays audio from a URL using the just_audio package. Include play/pause button, seek Slider, skip ±15s buttons, duration labels in mm:ss format, loading state handling, and proper dispose. Use StreamBuilder for reactive UI.

FlutterFlow Prompt

Create a page for a podcast episode detail. Add the episode title, description text from Firestore, and a container below for the audio player Custom Widget. Pass the episode's audioUrl field to the widget.

Frequently asked questions

Can I play audio in the background when the app is minimized?

Yes, but it requires additional setup. Add the audio_service package alongside just_audio and configure an AudioHandler. This is an advanced topic — the basic Custom Widget stops when the app goes to background.

What audio formats does just_audio support?

MP3, AAC, WAV, OGG, FLAC, and HLS streaming. MP3 and AAC are the most widely compatible across iOS and Android. HLS is useful for streaming long audio without downloading the full file.

How do I show album art or a waveform?

For album art: add an Image widget above the player controls, bound to the episode/track's imageUrl field. For waveform visualization: use the audio_waveforms package in a separate Custom Widget that reads the audio file's amplitude data.

Can I adjust playback speed?

Yes. Call player.setSpeed(1.5) for 1.5x speed. Add a speed selector button that cycles through 0.5x, 1.0x, 1.5x, 2.0x and display the current speed label.

Why does audio restart when I switch tabs in my app?

The widget is being disposed and recreated when you navigate. To keep audio playing across pages, lift the AudioPlayer instance to App State or use a global singleton. This requires Custom Code beyond the widget level.

Can RapidDev help with advanced audio features?

Yes. Background audio, offline caching, waveform visualization, and playlist management are complex implementations. RapidDev can build production-ready audio experiences for podcast, music, or education apps in FlutterFlow.

RapidDev

Talk to an Expert

Our team has built 600+ apps. Get personalized help with your project.

Book a free consultation

Need help with your project?

Our experts have built 600+ apps and can accelerate your development. Book a free consultation — no strings attached.

Book a free consultation

We put the rapid in RapidDev

Need a dedicated strategic tech and growth partner? Discover what RapidDev can do for your business! Book a call with our team to schedule a free, no-obligation consultation. We'll discuss your project and provide a custom quote at no cost.