Creating Meditation Apps with Flutter and Audio Playback

In today’s fast-paced world, meditation apps are becoming increasingly popular, offering a convenient way for users to find moments of peace and relaxation. Flutter, with its cross-platform capabilities and rich ecosystem, provides an excellent framework for developing such applications. Integrating audio playback is crucial for guiding meditations and enhancing the user experience. This article guides you through creating a meditation app using Flutter with integrated audio playback functionalities.

Why Flutter for Meditation Apps?

Flutter’s advantages make it an ideal choice for developing meditation apps:

  • Cross-Platform Development: Write code once and deploy to both iOS and Android.
  • Fast Development: Hot-reloading and expressive UI toolkit accelerate the development process.
  • Rich UI Components: Customize the user interface easily to create engaging and soothing experiences.
  • Audio Playback Libraries: Several plugins simplify audio management and playback.

Setting Up a New Flutter Project

First, let’s set up a new Flutter project. Open your terminal and run:

flutter create meditation_app

Navigate into the project directory:

cd meditation_app

Adding Dependencies

Add the following dependencies to your pubspec.yaml file. These packages will help us with audio playback, managing asynchronous tasks, and UI elements:

dependencies:
  flutter:
    sdk: flutter
  audioplayers: ^5.2.1 # Use the latest version
  cupertino_icons: ^1.0.2
  provider: ^6.0.0     # For state management

dev_dependencies:
  flutter_test:
    sdk: flutter
  flutter_lints: ^2.0.0

Run flutter pub get to install the dependencies.

Implementing Audio Playback with audioplayers

The audioplayers package allows us to easily manage audio playback in Flutter.

Step 1: Create an Audio Service Class

Create a new file called audio_service.dart. This service will manage audio playback states, control functions, and track progress.

import 'package:audioplayers/audioplayers.dart';
import 'package:flutter/foundation.dart';

class AudioService extends ChangeNotifier {
  final AudioPlayer _audioPlayer = AudioPlayer();
  String _currentAudioUrl = '';
  PlayerState _playerState = PlayerState.STOPPED;
  Duration _currentDuration = Duration.zero;
  Duration _totalDuration = Duration.zero;

  AudioPlayer get audioPlayer => _audioPlayer;
  PlayerState get playerState => _playerState;
  Duration get currentDuration => _currentDuration;
  Duration get totalDuration => _totalDuration;
  String get currentAudioUrl => _currentAudioUrl;

  AudioService() {
    _audioPlayer.onPlayerStateChanged.listen((state) {
      _playerState = state;
      notifyListeners();
    });

    _audioPlayer.onPositionChanged.listen((duration) {
      _currentDuration = duration;
      notifyListeners();
    });

    _audioPlayer.onDurationChanged.listen((duration) {
      _totalDuration = duration ?? Duration.zero;
      notifyListeners();
    });

    _audioPlayer.onPlayerComplete.listen((event) {
      _playerState = PlayerState.STOPPED;
      _currentDuration = Duration.zero;
      notifyListeners();
    });
  }

  Future play(String url) async {
    if (_currentAudioUrl != url) {
      await _audioPlayer.stop();
      await _audioPlayer.play(UrlSource(url));
      _currentAudioUrl = url;
    } else if (_playerState == PlayerState.PAUSED) {
      await _audioPlayer.resume();
    }
  }

  Future pause() async {
    await _audioPlayer.pause();
  }

  Future stop() async {
    await _audioPlayer.stop();
    _currentDuration = Duration.zero;
  }

  Future seek(Duration position) async {
    await _audioPlayer.seek(position);
  }

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

Step 2: Implement UI Components for Playback Controls

Next, update your main.dart file or create a new widget to display the audio playback controls. This UI will interact with the AudioService.

import 'package:flutter/material.dart';
import 'package:meditation_app/audio_service.dart';
import 'package:provider/provider.dart';
import 'package:audioplayers/audioplayers.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => AudioService(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Meditation App',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: MeditationScreen(),
    );
  }
}

class MeditationScreen extends StatelessWidget {
  final String audioUrl =
      'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'; // Replace with your audio URL

  @override
  Widget build(BuildContext context) {
    final audioService = Provider.of(context);

    return Scaffold(
      appBar: AppBar(title: Text('Meditation Audio')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Now Playing: Meditation Music'),
            SizedBox(height: 20),
            StreamBuilder(
              stream: audioService.audioPlayer.onPositionChanged,
              builder: (context, snapshot) {
                final duration = snapshot.data ?? Duration.zero;
                return Text('Current Time: ${duration.inSeconds}s');
              },
            ),
            StreamBuilder(
              stream: audioService.audioPlayer.onDurationChanged,
              builder: (context, snapshot) {
                final totalDuration = snapshot.data ?? Duration.zero;
                return Text('Total Duration: ${totalDuration.inSeconds}s');
              },
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                IconButton(
                  icon: Icon(Icons.play_arrow),
                  onPressed: () => audioService.play(audioUrl),
                ),
                IconButton(
                  icon: Icon(Icons.pause),
                  onPressed: () => audioService.pause(),
                ),
                IconButton(
                  icon: Icon(Icons.stop),
                  onPressed: () => audioService.stop(),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Integrating with Provider for State Management

We use the provider package for state management to ensure our UI reflects the audio player’s state accurately.

  1. Wrap Your App with ChangeNotifierProvider:
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => AudioService(),
      child: MyApp(),
    ),
  );
}
  1. Consume the AudioService:
final audioService = Provider.of<AudioService>(context);

Implementing Playback Controls

Add play, pause, and stop buttons to the UI. These buttons will call methods from our AudioService:

IconButton(
  icon: Icon(Icons.play_arrow),
  onPressed: () => audioService.play(audioUrl),
),
IconButton(
  icon: Icon(Icons.pause),
  onPressed: () => audioService.pause(),
),
IconButton(
  icon: Icon(Icons.stop),
  onPressed: () => audioService.stop(),
),

Displaying Current Playback Progress

Using StreamBuilder, we can listen to the onPositionChanged stream from the audioplayers instance to get real-time updates on the audio’s current position.

StreamBuilder<Duration>(
  stream: audioService.audioPlayer.onPositionChanged,
  builder: (context, snapshot) {
    final duration = snapshot.data ?? Duration.zero;
    return Text('Current Time: ${duration.inSeconds}s');
  },
),

Example: Adding a SeekBar for Control

Implement a SeekBar using a Slider widget in Flutter. It requires more integration and a callback for when the user interacts with the slider.

import 'package:flutter/material.dart';
import 'package:meditation_app/audio_service.dart';
import 'package:provider/provider.dart';
import 'package:audioplayers/audioplayers.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (context) => AudioService(),
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Meditation App',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: MeditationScreen(),
    );
  }
}

class MeditationScreen extends StatelessWidget {
  final String audioUrl =
      'https://www.soundhelix.com/examples/mp3/SoundHelix-Song-1.mp3'; // Replace with your audio URL

  @override
  Widget build(BuildContext context) {
    final audioService = Provider.of(context);

    return Scaffold(
      appBar: AppBar(title: Text('Meditation Audio')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text('Now Playing: Meditation Music'),
            SizedBox(height: 20),
            StreamBuilder(
              stream: audioService.audioPlayer.onPositionChanged,
              builder: (context, snapshot) {
                final duration = snapshot.data ?? Duration.zero;
                return Text('Current Time: ${duration.inSeconds}s');
              },
            ),
            StreamBuilder(
              stream: audioService.audioPlayer.onDurationChanged,
              builder: (context, snapshot) {
                final totalDuration = snapshot.data ?? Duration.zero;
                return Text('Total Duration: ${totalDuration.inSeconds}s');
              },
            ),
            Slider(
              value: audioService.currentDuration.inSeconds.toDouble(),
              max: audioService.totalDuration.inSeconds.toDouble(),
              onChanged: (value) {
                audioService.seek(Duration(seconds: value.toInt()));
              },
            ),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                IconButton(
                  icon: Icon(Icons.play_arrow),
                  onPressed: () => audioService.play(audioUrl),
                ),
                IconButton(
                  icon: Icon(Icons.pause),
                  onPressed: () => audioService.pause(),
                ),
                IconButton(
                  icon: Icon(Icons.stop),
                  onPressed: () => audioService.stop(),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

Additional Features to Consider

  • Offline Audio Support: Implement caching for audio files.
  • Background Playback: Enable audio playback even when the app is in the background.
  • Custom Meditations: Allow users to create and save custom meditation tracks.
  • UI/UX Improvements: Use soothing color schemes, animations, and intuitive navigation.

Conclusion

By combining Flutter’s robust development features with audio playback capabilities using the audioplayers package, you can create effective and engaging meditation apps. Properly integrating audio playback controls, displaying progress, and managing app state can greatly improve the user experience and create a tranquil environment for meditation practices.