Flutter Custom Notification Sounds: The Complete Cross‑Platform Guide

Add custom notification sounds to Flutter apps on Android and iOS with channels, file placement, FCM payloads, and testing tips.

ASOasis
7 min read
Flutter Custom Notification Sounds: The Complete Cross‑Platform Guide

Image used for representation purposes only.

Overview

Custom notification sounds can elevate the perceived quality of your Flutter app, help users distinguish message types, and reinforce your brand. Implementing them, however, differs significantly between Android and iOS, and between local and push (remote) notifications. This guide walks you through formats, file placement, Flutter code, Firebase Cloud Messaging (FCM) payloads, testing, and common pitfalls—so your sounds play reliably in production.

How notification sounds work on each platform

  • Android

    • For Android 8.0+ (API 26+), notification sound is defined by the notification channel. The sound cannot be changed once the channel is created. To change sounds later, create a new channel with a new ID.
    • Sound files live in android/app/src/main/res/raw/ and are typically .ogg, .mp3, or .wav.
    • For Android 7.x and below, you can still specify a sound per-notification, but on modern devices the channel decides.
  • iOS

    • Sound is specified per-notification (local or remote). Files must be packaged in the app bundle.
    • Supported formats include .aiff, .wav, and .caf. Keep sounds short (ideally <30s) and reasonably sized.

Choosing and preparing audio files

Best practices:

  • Keep it brief (0.5–2.5 seconds) and distinctive.
  • Use mono to reduce size; 44.1 kHz or 48 kHz sample rate; avoid clipping.
  • Normalize loudness to a comfortable level; respect user settings and Do Not Disturb.

Helpful conversions:

# Convert MP3 to mono WAV (PCM 16-bit)
ffmpeg -i input.mp3 -ac 1 -ar 44100 -c:a pcm_s16le ding.wav

# iOS-friendly CAF (IMA4 compressed) from WAV (macOS)
afconvert -f caff -d ima4 ding.wav ding.caf

# Android-friendly OGG (smaller than WAV)
ffmpeg -i ding.wav -c:a libvorbis -qscale:a 4 notify_custom.ogg

Placing files in your Flutter project

  • Android: put the file in android/app/src/main/res/raw/. Create raw if it doesn’t exist. Example: android/app/src/main/res/raw/notify_custom.ogg.
  • iOS: add the file to the Xcode project (Runner target) so it appears under “Copy Bundle Resources”. Example: Runner/ding.caf with Target Membership set to Runner.
  • Do not list these files under assets in pubspec.yaml; they must be packaged as platform resources/bundle files.

Local notifications with flutter_local_notifications

Add the plugins:

# pubspec.yaml (excerpt)
dependencies:
  flutter_local_notifications: ^latest
  firebase_messaging: ^latest   # optional, for FCM section later

Initialize and create a channel (Android). On iOS, request permissions and use a custom sound by name when scheduling/showing a notification.

import 'package:flutter_local_notifications/flutter_local_notifications.dart';

final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();

Future<void> initNotifications() async {
  const androidInit = AndroidInitializationSettings('@mipmap/ic_launcher');
  final iosInit = DarwinInitializationSettings(
    requestAlertPermission: true,
    requestBadgePermission: true,
    requestSoundPermission: true,
  );
  final initSettings = InitializationSettings(android: androidInit, iOS: iosInit);
  await flutterLocalNotificationsPlugin.initialize(initSettings);

  // ANDROID 8.0+: create a channel with a custom sound. The sound file must be in res/raw.
  const channelId = 'alerts_high';
  const channelName = 'Alerts (High)';
  const channelDescription = 'High-priority alerts with a custom sound';

  const androidChannel = AndroidNotificationChannel(
    channelId,
    channelName,
    description: channelDescription,
    importance: Importance.high,
    playSound: true,
    sound: RawResourceAndroidNotificationSound('notify_custom'), // notify_custom.ogg in res/raw
  );

  final androidPlugin = flutterLocalNotificationsPlugin
      .resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
  await androidPlugin?.createNotificationChannel(androidChannel);
}

Future<void> showLocalNotification() async {
  // For Android 8.0+, sound is taken from the channel. For older versions, the sound below applies.
  const androidDetails = AndroidNotificationDetails(
    'alerts_high',
    'Alerts (High)',
    channelDescription: 'High-priority alerts with a custom sound',
    importance: Importance.max,
    priority: Priority.high,
    playSound: true,
    sound: RawResourceAndroidNotificationSound('notify_custom'),
  );

  const iosDetails = DarwinNotificationDetails(
    sound: 'ding.caf', // file in the iOS app bundle
  );

  const details = NotificationDetails(android: androidDetails, iOS: iosDetails);

  await flutterLocalNotificationsPlugin.show(
    1001,
    'Order ready',
    'Your coffee is ready for pickup.',
    details,
  );
}

Notes:

  • If you change the Android channel sound after release, you must create a new channel ID. Existing installations keep the old channel.
  • On iOS, you specify the sound name each time; if it’s missing or not found, iOS falls back to the default sound (or silence if configured).

Push notifications with Firebase Cloud Messaging (FCM)

For remote notifications, you still need to package files into the app (Android res/raw, iOS bundle). Then configure payloads correctly.

Android via FCM

  • Android 8.0+: the channel determines the sound. Ensure your app creates a channel (with the desired sound) before a notification arrives. In Flutter, you can create the channel during app startup (as shown above) or in your background message handler.
  • Include the channel ID in the payload.

Background handler example to guarantee channel creation before display:

import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';

@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  final fln = FlutterLocalNotificationsPlugin();
  const channel = AndroidNotificationChannel(
    'alerts_high',
    'Alerts (High)',
    description: 'High-priority alerts with a custom sound',
    importance: Importance.high,
    playSound: true,
    sound: RawResourceAndroidNotificationSound('notify_custom'),
  );
  final androidPlugin = fln.resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>();
  await androidPlugin?.createNotificationChannel(channel);
}

Future<void> initFCM() async {
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
}

FCM HTTP v1 example for Android (channel controls the sound):

{
  "message": {
    "token": "DEVICE_TOKEN",
    "notification": {
      "title": "Order ready",
      "body": "Your coffee is ready for pickup."
    },
    "android": {
      "notification": {
        "channel_id": "alerts_high"
      }
    }
  }
}

Legacy HTTP example (pre-v1). The sound field may still be honored on Android < 8; on 8.0+ the channel wins:

{
  "to": "DEVICE_TOKEN",
  "notification": {
    "title": "Order ready",
    "body": "Your coffee is ready for pickup.",
    "sound": "notify_custom",
    "android_channel_id": "alerts_high"
  }
}

iOS via FCM (APNs payload)

Include the sound filename (with extension) in the aps payload. The file must be in the iOS app bundle.

HTTP v1 (platform override for APNs):

{
  "message": {
    "token": "DEVICE_TOKEN",
    "notification": {
      "title": "Order ready",
      "body": "Your coffee is ready for pickup."
    },
    "apns": {
      "payload": {
        "aps": {
          "sound": "ding.caf"
        }
      }
    }
  }
}

Notes:

  • To play different sounds for different message types, ship multiple files and set sound accordingly on iOS; on Android, create a dedicated channel per sound and select channel_id in the payload.
  • Foreground behavior on iOS depends on presentation options; you may need to set foreground presentation to include sound.

Verifying packaging and behavior

  • Android

    • Confirm the file exists in the APK/AAB: run a local build and check app/build/intermediates/merged_res/.../raw/ for your sound.
    • If the wrong sound plays, the channel likely already exists with a different sound. Uninstall the app or clear app storage, then reinstall. Shipping a new sound requires a brand-new channel ID.
    • Use Logcat to inspect notifications and confirm the channelId.
  • iOS

    • In Xcode, ensure your sound file appears under Build Phases → Copy Bundle Resources.
    • If no sound plays, verify the filename and extension in the payload exactly match the bundled file (case-sensitive on some systems).
    • Respect the Ring/Silent switch and Focus modes; normal notifications are silenced when the device is muted.

Common pitfalls and how to avoid them

  • Placing sound files under Flutter assets/ instead of platform bundles. Always use Android res/raw and iOS bundle resources.
  • Expecting per-notification sound changes on Android 8.0+. Use channels and plan the channel matrix early.
  • Using unsupported or oversized iOS audio. Prefer .aiff or .wav (PCM) for simplicity, or .caf with IMA4 for smaller size.
  • Forgetting to create the channel before receiving an FCM on Android. Create channels at startup and in background handlers.
  • Typos and missing extensions in the iOS sound field.

Designing a channel and sound strategy

Plan your taxonomy before launch:

  • Map each message category to a channel (Android) and a sound name (iOS).
  • Reserve room for future additions by using clear, versioned channel IDs, e.g., alerts_v1, promos_v1.
  • Document your payload contract for all senders (backend services, marketing tools) so they set the correct channel_id/sound.

Quick checklist

  • Audio prepared (short, normalized, mono) and converted to platform-friendly formats.
  • Android sound at android/app/src/main/res/raw/<name>.ogg|mp3|wav.
  • iOS sound added to Runner target and appears in Copy Bundle Resources.
  • flutter_local_notifications initialized; Android channel created with custom sound.
  • Local notifications play the expected sound on both platforms.
  • FCM payloads include channel_id (Android) and aps.sound (iOS) with exact filenames.
  • Tested on Android 8.0+ and iOS devices with various Focus/Do Not Disturb states.

Conclusion

Implementing custom notification sounds in Flutter is straightforward once you understand two rules: channels define sound on modern Android, and iOS requires a per-notification sound name that points to a bundled file. With the right file placement, a robust channel strategy, and carefully crafted FCM payloads, your notifications will sound exactly the way you designed—across platforms and versions.

Related Posts