Flutter Deep Dive: Implementing Seamless Audio Recording

Ahmed Ghaly
5 min readMar 15, 2024

--

In the realm of mobile app development, the integration of audio recording features has become increasingly crucial, especially for apps focusing on messaging, note-taking, or multimedia functionalities. Flutter, with its rich ecosystem and versatility, offers a straightforward path to embed audio recording capabilities into your app. In this article, we’ll embark on a detailed journey to implement an audio recording feature in a Flutter app from scratch. We’ll leverage a few powerful packages, craft a custom UI, and ensure users can record audio smoothly with just a few taps.

Prerequisites

Before diving into the code, it’s essential to equip our Flutter app with the necessary tools. This means adding specific dependencies to our pubspec.yaml file. Here, we're focusing on three major packages:

  1. record: Enables audio recording from the microphone to a file or stream.
  2. permission_handler: Handles cross-platform permissions, crucial for accessing the microphone.
  3. path_provider: Finds commonly used locations on the filesystem, such as temporary and app data directories.

Include these in your pubspec.yaml like so:

dependencies:
flutter:
sdk: flutter
record: ^5.0.4
permission_handler: ^11.3.0
path_provider: ^2.1.2

Running flutter pub get will fetch these packages and make them available in your project.

Crafting the UI Components

A captivating user interface is key to an engaging user experience. For our audio recording feature, we’ll design two custom widgets:

1. CustomRecordingWaveWidget

This widget simulates a dynamic wave effect, visually indicating that recording is in progress. It’s essentially a row of animated containers whose heights change over time to mimic sound waves.

import 'dart:async';
import 'package:flutter/material.dart';

class CustomRecordingWaveWidget extends StatefulWidget {
const CustomRecordingWaveWidget({super.key});

@override
State<CustomRecordingWaveWidget> createState() => _RecordingWaveWidgetState();
}

class _RecordingWaveWidgetState extends State<CustomRecordingWaveWidget> {
final List<double> _heights = [0.05, 0.07, 0.1, 0.07, 0.05];
Timer? _timer;

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

@override
void dispose() {
_timer?.cancel();
super.dispose();
}

void _startAnimating() {
_timer = Timer.periodic(const Duration(milliseconds: 150), (timer) {
setState(() {
// This is a simple way to rotate the list, creating a wave effect.
_heights.add(_heights.removeAt(0));
});
});
}

@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.sizeOf(context).height * 0.1,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: _heights.map((height) {
return AnimatedContainer(
duration: const Duration(milliseconds: 300),
width: 20,
height: MediaQuery.sizeOf(context).height * height,
margin: const EdgeInsets.only(right: 10),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(50),
),
);
}).toList(),
),
);
}
}

2. CustomRecordingButton

A button that toggles between recording and not recording states. Its design changes based on the recording status to give immediate visual feedback to the user.

import 'package:flutter/material.dart';

class CustomRecordingButton extends StatelessWidget {
const CustomRecordingButton({
super.key,
required this.isRecording,
required this.onPressed,
});

final bool isRecording;
final VoidCallback onPressed;

@override
Widget build(BuildContext context) {
return AnimatedContainer(
height: 100,
width: 100,
duration: const Duration(milliseconds: 300),
padding: EdgeInsets.all(
isRecording ? 25 : 15,
),
decoration: BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
border: Border.all(
color: Colors.blue,
width: isRecording ? 8 : 3,
),
),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
height: 70,
width: 70,
decoration: BoxDecoration(
color: Colors.blue,
shape: isRecording ? BoxShape.rectangle : BoxShape.circle,
),
child: MaterialButton(
onPressed: onPressed,
shape: const CircleBorder(),
child: const SizedBox.shrink(),
),
),
);
}
}

The Heart of the Operation: RecordingScreen

The RecordingScreen widget ties our UI components to the actual recording functionality. It handles starting and stopping the recording, managing permissions, and updating the UI based on the recording state.

import 'dart:math';
import 'package:flutter/material.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:record/record.dart';

class RecordingScreen extends StatefulWidget {
const RecordingScreen({super.key});

@override
State<RecordingScreen> createState() => _RecordingScreenState();
}

class _RecordingScreenState extends State<RecordingScreen> {
bool isRecording = false;
late final AudioRecorder _audioRecorder;
String? _audioPath;

@override
void initState() {
_audioRecorder = AudioRecorder();
super.initState();
}

@override
void dispose() {
_audioRecorder.dispose();
super.dispose();
}

String _generateRandomId() {
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
final random = Random();
return List.generate(
10,
(index) => chars[random.nextInt(chars.length)],
growable: false,
).join();
}

Future<void> _startRecording() async {
try {
debugPrint(
'=========>>>>>>>>>>> RECORDING!!!!!!!!!!!!!!! <<<<<<===========');

String filePath = await getApplicationDocumentsDirectory()
.then((value) => '${value.path}/${_generateRandomId()}.wav');

await _audioRecorder.start(
const RecordConfig(
// specify the codec to be `.wav`
encoder: AudioEncoder.wav,
),
path: filePath,
);
} catch (e) {
debugPrint('ERROR WHILE RECORDING: $e');
}
}

Future<void> _stopRecording() async {
try {
String? path = await _audioRecorder.stop();

setState(() {
_audioPath = path!;
});
debugPrint('=========>>>>>> PATH: $_audioPath <<<<<<===========');
} catch (e) {
debugPrint('ERROR WHILE STOP RECORDING: $e');
}
}

void _record() async {
if (isRecording == false) {
final status = await Permission.microphone.request();

if (status == PermissionStatus.granted) {
setState(() {
isRecording = true;
});
await _startRecording();
} else if (status == PermissionStatus.permanentlyDenied) {
debugPrint('Permission permanently denied');
// TODO: handle this case
}
} else {
await _stopRecording();

setState(() {
isRecording = false;
});
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
if (isRecording) const CustomRecordingWaveWidget(),
const SizedBox(height: 16),
CustomRecordingButton(
isRecording: isRecording,
onPressed: () => _record(),
),
],
),
);
}
}

Key Functions:

_startRecording(): Initiates the recording process.

  • Requests microphone permission using Permission.microphone.request().
  • Configures recording settings to use the WAV format.
  • Starts recording with _audioRecorder.start().
  • Generates a unique filename for the recording and saves the recording’s file path.

_stopRecording(): Ends the recording session.

  • Stops the recording with _audioRecorder.stop().
  • Updates the _audioPath state with the path of the saved recording file.

_record(): Main function linked to the record button.

  • Checks the current isRecording state to determine action.

If not already recording:

  • Requests microphone permission.
  • If permission is granted, starts recording and updates isRecording to true.

If currently recording:

  • Stops the recording.
  • Updates isRecording to false.

Additional Resources:

In Conclusion

Integrating audio recording into your Flutter app doesn’t have to be a daunting task. With the right packages and a bit of custom UI magic, you can implement a seamless audio recording feature that enhances your app’s functionality and user experience. Whether for capturing voice notes, enabling in-app communication, or any other audio-related feature, the approach outlined in this article lays down a solid foundation to build upon.

Remember, while this guide gets you up and running, there’s always room to expand and customize the feature to better suit your app’s needs. Happy coding!

--

--