Flutter中如何实现FCM后台推送时循环播放自定义通知音效?
问题分析与解决方案
核心问题根源
- FCM后台Isolate隔离:FCM的
_firebaseMessagingBackgroundHandler运行在独立的后台Isolate中,和主应用Isolate完全隔离。你在main.dart的initState里创建的AlarmPlayer实例,以及注册的onPlayerStateChanged监听,仅存在于主Isolate,无法捕获后台Isolate中播放器的状态变化。 - 后台音频权限缺失:Android和iOS默认限制应用在后台播放音频,未配置对应权限的话,音频播放完毕后进程会被系统回收,循环逻辑无法执行。
- 手动循环不可靠:依赖
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
相关产品推荐
相关产品推荐

