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

Flutter开发:实现后台/杀死状态下常驻Heads Up通知

Flutter实现后台/杀死状态下的常驻Heads Up通知(类似Uber接单通知)

要实现像Uber司机端那样的后台/杀死状态下触发Heads Up通知、持续播放提示音、常驻屏幕直至用户操作的效果,需要针对Android和iOS分别做平台特定配置,结合FCM(Firebase Cloud Messaging)和本地通知插件来实现。

核心思路

  • 后台/杀死状态下,依赖推送服务(FCM/APNs)的数据消息而非通知消息,这样能完全自定义通知行为
  • Heads Up通知需要设置高优先级、全屏意图(Android)或强制弹窗权限(iOS)
  • 持续提示音无法通过系统通知声音实现(默认仅播放一次),需配合前台服务(Android)或后台音频任务(iOS)循环播放

Android 实现步骤

1. 配置权限与依赖

在android/app/src/main/AndroidManifest.xml中添加必要权限:

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />

在pubspec.yaml中添加依赖:

dependencies:
  firebase_messaging: ^14.6.1
  flutter_local_notifications: ^14.1.1
  audioplayers: ^4.0.1 # 用于播放循环提示音

2. 创建高优先级通知渠道

必须创建IMPORTANCE_HIGH级别的渠道,才能触发Heads Up:

final AndroidNotificationChannel _channel = AndroidNotificationChannel(
  'urgent_notification_channel',
  '紧急通知渠道',
  description: '用于接单类高优先级通知',
  importance: Importance.high,
  sound: RawResourceAndroidNotificationSound('order_alert'), // 自定义提示音,放在res/raw下
  enableVibration: true,
  vibrationPattern: Int64List.fromList([0, 1000, 500, 1000]),
);

// 初始化时创建渠道
await FlutterLocalNotificationsPlugin().resolvePlatformSpecificImplementation<AndroidFlutterLocalNotificationsPlugin>()?.createNotificationChannel(_channel);

3. 处理FCM后台消息

杀死/后台状态下,只有data类型的FCM消息会触发onBackgroundMessage回调,在这里构建常驻Heads Up通知:

Future<void> _firebaseBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  final flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
  
  // 初始化本地通知
  await flutterLocalNotificationsPlugin.initialize(
    InitializationSettings(
      android: AndroidInitializationSettings('@mipmap/ic_launcher'),
    ),
  );

  // 显示常驻Heads Up通知
  await flutterLocalNotificationsPlugin.show(
    int.parse(message.data['notification_id']),
    message.data['title'],
    message.data['body'],
    NotificationDetails(
      android: AndroidNotificationDetails(
        _channel.id,
        _channel.name,
        channelDescription: _channel.description,
        importance: Importance.high,
        priority: Priority.high,
        visibility: NotificationVisibility.public,
        fullScreenIntent: true, // 强制弹出全屏Heads Up
        ongoing: true, // 常驻通知,无法滑动取消
        autoCancel: false, // 手动操作后才取消
        sound: RawResourceAndroidNotificationSound('order_alert'),
      ),
    ),
    payload: json.encode(message.data),
  );

  // 启动循环提示音
  _startLoopSound();
}

// 循环播放提示音
void _startLoopSound() async {
  final player = AudioPlayer();
  await player.setAsset('assets/sounds/order_alert.wav');
  await player.setLoopMode(LoopMode.one);
  await player.play();
  // 需保存player实例,在用户操作时停止
}

4. 处理通知操作

当用户点击或取消通知时,停止提示音并取消通知:

flutterLocalNotificationsPlugin.initialize(
  InitializationSettings(...),
  onDidReceiveNotificationResponse: (response) async {
    // 停止声音
    await player.stop();
    // 取消常驻通知
    await flutterLocalNotificationsPlugin.cancel(int.parse(response.payload!['notification_id']));
  },
);

iOS 实现步骤

1. 配置权限与后台模式

在Info.plist中添加:

<key>UIBackgroundModes</key>
<array>
  <string>remote-notification</string>
  <string>audio</string> <!-- 用于后台播放声音 -->
</array>
<key>NSRemoteNotificationUsageDescription</key>
<string>接收订单通知</string>
<key>UNAuthorizationOptions</key>
<array>
  <string>alert</string>
  <string>sound</string>
  <string>badge</string>
</array>

2. 配置APNs与通知处理

在AppDelegate.swift中注册通知并设置代理:

import UserNotifications
import Firebase

@UIApplicationMain
class AppDelegate: FlutterAppDelegate, UNUserNotificationCenterDelegate {
  override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    FirebaseApp.configure()
    
    // 请求通知权限
    UNUserNotificationCenter.current().delegate = self
    let authOptions: UNAuthorizationOptions = [.alert, .sound, .badge]
    UNUserNotificationCenter.current().requestAuthorization(options: authOptions) { _, _ in }
    application.registerForRemoteNotifications()
    
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }

  // 后台/杀死状态下接收通知时触发
  override func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
    // 构建自定义Heads Up通知
    let content = UNMutableNotificationContent()
    content.title = userInfo["title"] as? String ?? ""
    content.body = userInfo["body"] as? String ?? ""
    content.sound = UNNotificationSound(named: UNNotificationSoundName(rawValue: "order_alert.wav"))
    content.badge = 1
    content.categoryIdentifier = "urgent_order"
    
    let request = UNNotificationRequest(identifier: userInfo["notification_id"] as? String ?? "", content: content, trigger: nil)
    UNUserNotificationCenter.current().add(request)
    
    // 启动后台音频播放
    startLoopSound()
    
    completionHandler(.newData)
  }

  // 前台显示Heads Up
  func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
    completionHandler([.alert, .sound, .badge])
  }

  // 用户操作通知时停止声音
  func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
    stopLoopSound()
    completionHandler()
  }

  // 音频播放相关方法
  var audioPlayer: AVAudioPlayer?
  func startLoopSound() {
    guard let url = Bundle.main.url(forResource: "order_alert", withExtension: "wav") else { return }
    do {
      audioPlayer = try AVAudioPlayer(contentsOf: url)
      audioPlayer?.numberOfLoops = -1 // 无限循环
      audioPlayer?.play()
    } catch {
      print("播放失败: \(error)")
    }
  }

  func stopLoopSound() {
    audioPlayer?.stop()
    audioPlayer = nil
  }
}

3. 注意事项

  • iOS的通知声音最长支持30秒,若需要更长时间循环,需依赖后台音频任务
  • 杀死状态下,只有当应用被APNs唤醒时才能执行代码,需确保推送消息包含content-available: 1字段

关键注意点

  • 推送消息必须使用data payload(Android)或包含content-available: 1(iOS),才能在杀死状态下触发自定义逻辑
  • Android的fullScreenIntent需要API 26+,低版本可使用priority: Priority.max替代
  • 自定义提示音需放在对应平台的资源目录(Android: res/raw;iOS: 主Bundle)

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.11 22:42:15