Flutterexperts

Empowering Vision with FlutterExperts' Expertise

FlutterFlow has revolutionized the way we build cross-platform apps by offering a low-code platform that blends intuitive UI design with powerful backend integrations. One of the best examples of this is building a Music Player App using the just_audio package. This blog will guide you through how to create a fully functional music player with playback controls, seekbar, and streaming capabilities directly inside FlutterFlow.

Why Choose FlutterFlow + just_audio?

  • Rapid prototyping: Get to MVP fast without managing boilerplate code.
  • Visual development: Drag-and-drop UI without sacrificing control.
  • Native performance: Just_audio enables gapless playback and efficient streaming.
  • Customizable components: Integrate custom widgets for complete player control.

Features of the Music Player App

  • Play/Pause music
  • Display track duration and progress
  • Seek bar to skip through audio
  • Volume and playback speed adjustment
  • Podcast and music URL support
  • Fully customizable UI

Getting Started

Step 1: Set Up FlutterFlow Project

  1. Create a new FlutterFlow project.
  2. Enable Custom Widgets and Custom Code.
  3. Add the just_audio package in the pubspec.yaml section:
just_audio: ^0.9.35

Step 2: Create the UI Layout

  • Design a simple layout using the following elements:
  • Buttons for Play/Pause, Volume, Speed
  • Image widget (album art)
  • Text widgets for track name and duration
  • Slider for seekbar

Step 3: Create Custom Widget for Audio Player

Navigate to Custom Widgets and create a new widget: Make a Custom widget.

Code: 

// Automatic FlutterFlow imports
import '/flutter_flow/flutter_flow_theme.dart';
import '/flutter_flow/flutter_flow_util.dart';
import '/custom_code/widgets/index.dart'; // Imports other custom widgets
import '/flutter_flow/custom_functions.dart'; // Imports custom functions
import 'package:flutter/material.dart';
// Begin custom widget code
// DO NOT REMOVE OR MODIFY THE CODE ABOVE!

import 'package:audio_session/audio_session.dart';
import 'package:flutter/services.dart';
import 'package:just_audio/just_audio.dart';
import 'package:rxdart/rxdart.dart';

class AudioWidget extends StatefulWidget {
const AudioWidget({
super.key,
this.width,
this.height,
this.audioUrl,
});

final double? width;
final double? height;
final String? audioUrl;

@override
State<AudioWidget> createState() => _AudioWidgetState();
}

class _AudioWidgetState extends State<AudioWidget> {
final _player = AudioPlayer();

@override
void initState() {
super.initState();
_init();
}

Future<void> _init() async {
// Inform the operating system of our app's audio attributes etc.
final session = await AudioSession.instance;
await session.configure(const AudioSessionConfiguration.speech());

// Try to load audio from a source and catch any errors.
try {
await _player
.setAudioSource(AudioSource.uri(Uri.parse(widget.audioUrl!)));
} catch (e) {
print("Error loading audio source: $e");
}
}



@override
void dispose() {
// Release decoders and buffers back to the operating system making them
// available for other apps to use.
_player.dispose();
super.dispose();
}

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
if (state == AppLifecycleState.paused) {
// Release the player's resources when not in use. We use "stop" so that
// if the app resumes later, it will still remember what position to
// resume from.
_player.stop();
}
}

/// Collects the data useful for displaying in a seek bar, using a handy
/// feature of rx_dart to combine the 3 streams of interest into one.
Stream<PositionData> get _positionDataStream =>
Rx.combineLatest3<Duration, Duration, Duration?, PositionData>(
_player.positionStream,
_player.bufferedPositionStream,
_player.durationStream,
(position, bufferedPosition, duration) => PositionData(
position, bufferedPosition, duration ?? Duration.zero));

@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: SafeArea(
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
// Display play/pause button and volume/speed sliders.
ControlButtons(_player),
// Display seek bar. Using StreamBuilder, this widget rebuilds
// each time the position, buffered position or duration changes.
StreamBuilder<PositionData>(
stream: _positionDataStream,
builder: (context, snapshot) {
final positionData = snapshot.data;
return SeekBar(
duration: positionData?.duration ?? Duration.zero,
position: positionData?.position ?? Duration.zero,
bufferedPosition:
positionData?.bufferedPosition ?? Duration.zero,
onChangeEnd: _player.seek,
);
},
),
],
),
),
),
);
}
}

/// Displays the play/pause button and volume/speed sliders.
class ControlButtons extends StatelessWidget {
final AudioPlayer player;

const ControlButtons(this.player, {Key? key}) : super(key: key);

@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
// Opens volume slider dialog
IconButton(
icon: const Icon(Icons.volume_up),
onPressed: () {
showSliderDialog(
context: context,
title: "Adjust volume",
divisions: 10,
min: 0.0,
max: 1.0,
value: player.volume,
stream: player.volumeStream,
onChanged: player.setVolume,
);
},
),

/// This StreamBuilder rebuilds whenever the player state changes, which
/// includes the playing/paused state and also the
/// loading/buffering/ready state. Depending on the state we show the
/// appropriate button or loading indicator.
StreamBuilder<PlayerState>(
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: const EdgeInsets.all(8.0),
width: 64.0,
height: 64.0,
child: const CircularProgressIndicator(),
);
} else if (playing != true) {
return IconButton(
icon: const Icon(Icons.play_arrow),
iconSize: 64.0,
onPressed: player.play,
);
} else if (processingState != ProcessingState.completed) {
return IconButton(
icon: const Icon(Icons.pause),
iconSize: 64.0,
onPressed: player.pause,
);
} else {
return IconButton(
icon: const Icon(Icons.replay),
iconSize: 64.0,
onPressed: () => player.seek(Duration.zero),
);
}
},
),
// Opens speed slider dialog
StreamBuilder<double>(
stream: player.speedStream,
builder: (context, snapshot) => IconButton(
icon: Text("${snapshot.data?.toStringAsFixed(1)}x",
style: const TextStyle(fontWeight: FontWeight.bold)),
onPressed: () {
showSliderDialog(
context: context,
title: "Adjust speed",
divisions: 10,
min: 0.5,
max: 1.5,
value: player.speed,
stream: player.speedStream,
onChanged: player.setSpeed,
);
},
),
),
],
);
}
}

class SeekBar extends StatefulWidget {
final Duration duration;
final Duration position;
final Duration bufferedPosition;
final ValueChanged<Duration>? onChanged;
final ValueChanged<Duration>? onChangeEnd;

const SeekBar({
Key? key,
required this.duration,
required this.position,
required this.bufferedPosition,
this.onChanged,
this.onChangeEnd,
}) : super(key: key);

@override
SeekBarState createState() => SeekBarState();
}

class SeekBarState extends State<SeekBar> {
double? _dragValue;
late SliderThemeData _sliderThemeData;

@override
void didChangeDependencies() {
super.didChangeDependencies();

_sliderThemeData = SliderTheme.of(context).copyWith(
trackHeight: 2.0,
);
}

@override
Widget build(BuildContext context) {
return Stack(
children: [
SliderTheme(
data: _sliderThemeData.copyWith(
thumbShape: RoundSliderThumbShape(enabledThumbRadius: 0),
activeTrackColor: Colors.blue.shade100,
inactiveTrackColor: Colors.grey.shade300,
),
child: ExcludeSemantics(
child: Slider(
min: 0.0,
max: widget.duration.inMilliseconds.toDouble(),
value: min(widget.bufferedPosition.inMilliseconds.toDouble(),
widget.duration.inMilliseconds.toDouble()),
onChanged: (value) {
setState(() {
_dragValue = value;
});
if (widget.onChanged != null) {
widget.onChanged!(Duration(milliseconds: value.round()));
}
},
onChangeEnd: (value) {
if (widget.onChangeEnd != null) {
widget.onChangeEnd!(Duration(milliseconds: value.round()));
}
_dragValue = null;
},
),
),
),
SliderTheme(
data: _sliderThemeData.copyWith(
inactiveTrackColor: Colors.transparent,
),
child: Slider(
min: 0.0,
max: widget.duration.inMilliseconds.toDouble(),
value: min(_dragValue ?? widget.position.inMilliseconds.toDouble(),
widget.duration.inMilliseconds.toDouble()),
onChanged: (value) {
setState(() {
_dragValue = value;
});
if (widget.onChanged != null) {
widget.onChanged!(Duration(milliseconds: value.round()));
}
},
onChangeEnd: (value) {
if (widget.onChangeEnd != null) {
widget.onChangeEnd!(Duration(milliseconds: value.round()));
}
_dragValue = null;
},
),
),
Positioned(
right: 16.0,
bottom: 0.0,
child: Text(
RegExp(r'((^0*[1-9]\d*:)?\d{2}:\d{2})\.\d+$')
.firstMatch("$_remaining")
?.group(1) ??
'$_remaining',
style: Theme.of(context).textTheme.bodySmall),
),
],
);
}

Duration get _remaining => widget.duration - widget.position;
}

class PositionData {
final Duration position;
final Duration bufferedPosition;
final Duration duration;

PositionData(this.position, this.bufferedPosition, this.duration);
}

void showSliderDialog({
required BuildContext context,
required String title,
required int divisions,
required double min,
required double max,
String valueSuffix = '',
// TODO: Replace these two by ValueStream.
required double value,
required Stream<double> stream,
required ValueChanged<double> onChanged,
}) {
showDialog<void>(
context: context,
builder: (context) => AlertDialog(
title: Text(title, textAlign: TextAlign.center),
content: StreamBuilder<double>(
stream: stream,
builder: (context, snapshot) => SizedBox(
height: 100.0,
child: Column(
children: [
Text('${snapshot.data?.toStringAsFixed(1)}$valueSuffix',
style: const TextStyle(
fontFamily: 'Fixed',
fontWeight: FontWeight.bold,
fontSize: 24.0)),
Slider(
divisions: divisions,
min: min,
max: max,
value: snapshot.data ?? value,
onChanged: onChanged,
),
],
),
),
),
),
);
}

T? ambiguate<T>(T? value) => value;

Use AudioPlayer from just_audio and handle play/pause, seek, and buffering states. Integrate stream listeners to manage UI updates.

Step 4: Add the Widget to Your Page

  1. Drag a Custom Widget component onto your FlutterFlow page.
  2. Choose AudioWidget from the list.
  3. Pass a public MP3 or podcast URL to the audioUrl property.

Extra Tips

  • Test your URLs — ensure the audio link supports direct streaming.
  • Handle lifecycle events: stop audio on app pause.
  • Use audio_session for better background playback control.
  • Add animation to Play/Pause buttons for a polished UI.

Use Cases

  • Music streaming apps
  • Meditation & wellness apps
  • Learning platforms (audio courses)
  • Podcast players

Conclusion

FlutterFlow + just_audio offers a powerful combination for building audio-rich apps without diving into boilerplate code. By leveraging FlutterFlow’s visual builder and just_audio’s audio streaming capabilities, developers can create sleek, responsive, and customizable music players for any type of app.

From Our Parent Company Aeologic

Aeologic Technologies is a leading AI-driven digital transformation company in India, helping businesses unlock growth with AI automation, IoT solutions, and custom web & mobile app development. We also specialize in AIDC solutions and technical manpower augmentation, offering end-to-end support from strategy and design to deployment and optimization.

Trusted across industries like manufacturing, healthcare, logistics, BFSI, and smart cities, Aeologic combines innovation with deep industry expertise to deliver future-ready solutions.

Feel free to connect with us:
And read more articles from FlutterDevs.com.

FlutterDevs team of Flutter developers to build high-quality and functionally-rich apps. Hire Flutterflow developer for your cross-platform Flutter mobile app project on an hourly or full-time basis as per your requirement! For any flutter-related queries, you can connect with us on Facebook, GitHub, Twitter, and LinkedIn.

We welcome feedback and hope that you share what you’re working on using #FlutterDevs. We truly enjoy seeing how you use Flutter to build beautiful, interactive web experiences.

Leave comment

Your email address will not be published. Required fields are marked with *.