You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Flutter中如何实现FCM后台推送时循环播放自定义通知音效?

问题分析与解决方案

核心问题根源

  1. FCM后台Isolate隔离:FCM的_firebaseMessagingBackgroundHandler运行在独立的后台Isolate中,和主应用Isolate完全隔离。你在main.dart的initState里创建的AlarmPlayer实例,以及注册的onPlayerStateChanged监听,仅存在于主Isolate,无法捕获后台Isolate中播放器的状态变化。
  2. 后台音频权限缺失:Android和iOS默认限制应用在后台播放音频,未配置对应权限的话,音频播放完毕后进程会被系统回收,循环逻辑无法执行。
  3. 手动循环不可靠:依赖completed事件手动触发循环的逻辑,在后台Isolate环境下无法正常工作,因为状态监听无法跨Isolate生效。

具体修复步骤

1. 配置后台音频权限

Android端

在android/app/src/main/AndroidManifest.xml中添加权限与服务配置:

<!-- 添加前台服务与音频权限 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA" />

<application ...>
  <!-- 注册音频播放服务,声明前台服务类型 -->
  <service
      android:name="xyz.luan.audioplayers.AudioPlayerService"
      android:foregroundServiceType="mediaPlayback" />
</application>
iOS端

在ios/Runner/Info.plist中添加后台模式:

<key>UIBackgroundModes</key>
<array>
  <string>audio</string>
  <string>remote-notification</string>
</array>

2. 修改AlarmPlayer,使用插件原生循环功能

放弃手动监听completed事件的循环逻辑,改用audioplayers自带的ReleaseMode.loop实现后台循环,前台保持单次播放:

import 'package:audioplayers/audioplayers.dart';

enum notiState { background, foreground, unknown }

class AlarmPlayer {
  late bool isPlaying;
  late bool isResponded;
  late AudioPlayer player;
  late notiState state;

  AlarmPlayer() {
    isPlaying = false;
    isResponded = true;
    player = AudioPlayer();
    state = notiState.unknown;
  }

  void init() {
    // 仅保留前台状态监听,用于前台逻辑
    player.onPlayerStateChanged.listen((event) {
      switch (event) {
        case PlayerState.playing:
          isPlaying = true;
          break;
        case PlayerState.stopped:
        case PlayerState.completed:
          isPlaying = false;
          break;
        default:
          break;
      }
    });
  }

  Future<void> playAlarm() async {
    if (!isPlaying) {
      // 根据状态设置播放模式
      await player.setReleaseMode(
        state == notiState.background ? ReleaseMode.loop : ReleaseMode.stop,
      );
      await player.play(AssetSource('audio/emergency.mp3'));
    }
  }

  Future<void> stopAlarm() async {
    await player.stop();
    // 重置播放模式,避免影响下次播放
    await player.setReleaseMode(ReleaseMode.stop);
    isPlaying = false;
  }
}

3. 解决FCM后台Isolate通信问题

通过IsolateNameServer实现跨Isolate通信,让后台handler通知主Isolate启动音频播放:

main.dart初始化逻辑
import 'dart:isolate';
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';

late AlarmPlayer alarmPlayer;

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  // 注册主Isolate的通信端口
  final receivePort = ReceivePort();
  IsolateNameServer.registerPortByName('main_app_port', receivePort.sendPort);

  // 监听后台Isolate的消息
  receivePort.listen((message) {
    if (message == 'trigger_background_alarm') {
      alarmPlayer.state = notiState.background;
      alarmPlayer.isResponded = false;
      alarmPlayer.playAlarm();
    }
  });

  alarmPlayer = AlarmPlayer();
  alarmPlayer.init();

  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  runApp(const MyApp());
}
修改FCM后台消息处理器
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();

  final String? msgTitle = message.notification!.title;
  final String? msgBody = message.notification!.body;
  if (msgTitle != null && msgBody != null) {
    // 获取主Isolate的通信端口,发送触发指令
    final mainPort = IsolateNameServer.lookupPortByName('main_app_port');
    mainPort?.send('trigger_background_alarm');
  }
}

4. 完善停止逻辑

确保应用切换前台、点击通知时能立即停止音频:

// 在主页面的initState中添加监听
@override
void initState() {
  super.initState();

  // 应用从后台切前台时停止音频
  WidgetsBinding.instance.addObserver(_LifecycleObserver(
    onResume: () {
      alarmPlayer.isResponded = true;
      alarmPlayer.stopAlarm();
    },
  ));

  // 点击通知打开应用时停止音频
  FirebaseMessaging.onMessageOpenedApp.listen((_) {
    alarmPlayer.isResponded = true;
    alarmPlayer.stopAlarm();
  });

  // 前台接收消息时的处理
  FirebaseMessaging.onMessage.listen((message) {
    alarmPlayer.state = notiState.foreground;
    alarmPlayer.isResponded = false;
    alarmPlayer.playAlarm();
  });
}

// 自定义生命周期观察者
class _LifecycleObserver with WidgetsBindingObserver {
  final VoidCallback onResume;

  _LifecycleObserver({required this.onResume});

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.resumed) onResume();
  }
}

内容的提问来源于stack exchange,提问作者tamptek

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 19:02:25