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

Flutter自定义本地通知iOS端无声音问题排查求助

Flutter iOS端自定义通知声音不生效问题排查与解决

问题描述

  • Android端使用本地通知包自定义通知声音正常,但iOS端接收通知时无法播放自定义声音。
  • 用Postman向https://fcm.googleapis.com/fcm/send发送以下请求时,iOS设备能正常播放指定的自定义声音:
{
    "to" : "My device Id",
    "notification" : {
        "title" : "Sound test",
        "body" : "Sound body",
        "sound" : "sound.wav"
    }
}
  • 代码实现全局自定义声音时,通知能正常接收,但无自定义声音输出。经排查,问题出在firebaseInit函数的Platform.isIOS分支——该分支仅调用了forgroundMessage方法,未执行iOS相关的本地通知初始化与展示逻辑。

相关代码

NotificationServices类完整代码

class NotificationServices {
  CustomerViewModel customerViewModel = CustomerViewModel();
  FirebaseMessaging messaging = FirebaseMessaging.instance;
  final FlutterLocalNotificationsPlugin _flutterLocalNotificationsPlugin =
      FlutterLocalNotificationsPlugin();

  void initLocalNotifications(
      BuildContext context, RemoteMessage message) async {
    var androidInitializationSettings =
        const AndroidInitializationSettings('@drawable/ic_launcher');
    var iosInitializationSettings = const DarwinInitializationSettings(
      requestAlertPermission: true,
      requestBadgePermission: true,
      requestSoundPermission: true,
    );

    var initializationSetting = InitializationSettings(
        android: androidInitializationSettings, iOS: iosInitializationSettings);

    await _flutterLocalNotificationsPlugin.initialize(initializationSetting,
        onDidReceiveNotificationResponse: (payload) {
      handleMessage(context, message);
    });
  }

  void firebaseInit(BuildContext context) {
    FirebaseMessaging.onMessage.listen((message) {
      RemoteNotification? notification = message.notification;
      AndroidNotification? android = message.notification!.android;

      if (kDebugMode) {
        print("notifications title:${notification!.title}");
        print("notifications body:${notification.body}");
        print('count:${android!.count}');
        print('data:${message.data.toString()}');
      }

      if (Platform.isIOS) {
        forgroundMessage();
      }

      if (Platform.isAndroid) {
        initLocalNotifications(context, message);
        showNotification(message);
      }
    });
  }

  void requestNotificationPermission() async {
    NotificationSettings settings = await messaging.requestPermission(
      alert: true,
      announcement: true,
      badge: true,
      carPlay: true,
      criticalAlert: true,
      provisional: false,
      sound: true,
    );

    if (settings.authorizationStatus == AuthorizationStatus.authorized) {
      if (kDebugMode) {
        print('user granted permission');
      }
    } else if (settings.authorizationStatus ==
        AuthorizationStatus.provisional) {
      if (kDebugMode) {
        print('user granted provisional permission');
      }
    } else {
      if (kDebugMode) {
        print('user denied permission');
      }
    }
  }

  Future<void> showNotification(RemoteMessage message) async {
    AndroidNotificationChannel channel = AndroidNotificationChannel(
        message.notification!.android!.channelId.toString(),
        message.notification!.android!.channelId.toString(),
        importance: Importance.max,
        showBadge: true,
        playSound: true,
        sound: const RawResourceAndroidNotificationSound('notification'));

    AndroidNotificationDetails androidNotificationDetails =
        AndroidNotificationDetails(
            'bana_lazimsin_channel', 'High Importance Notifications',
            channelDescription:
                'This channel is used for important notifications',
            importance: Importance.high,
            priority: Priority.high,
            playSound: true,
            ticker: 'ticker',
            icon: '@drawable/ic_launcher',
            color: const Color.fromARGB(255, 255, 15, 123),
            sound: channel.sound);
    DarwinNotificationDetails darwinNotificationDetails =
        const DarwinNotificationDetails(sound: 'notification.wav');

    NotificationDetails notificationDetails = NotificationDetails(
        android: androidNotificationDetails, iOS: darwinNotificationDetails);

    Future.delayed(Duration.zero, () {
      _flutterLocalNotificationsPlugin.show(
        0,
        message.notification!.title.toString(),
        message.notification!.body.toString(),
        notificationDetails,
      );
    });
  }

  Future<String> getDeviceToken() async {
    String? token = await messaging.getToken();
    return token!;
  }

  void isTokenRefresh() async {
    messaging.onTokenRefresh.listen((event) {
      event.toString();
      if (kDebugMode) {
        print('refresh');
      }
    });
  }

  Future<void> setupInteractMessage(BuildContext context) async {
    RemoteMessage? initialMessage =
        await FirebaseMessaging.instance.getInitialMessage();

    if (initialMessage != null) {
      handleMessage(context, initialMessage);
    }

    FirebaseMessaging.onMessageOpenedApp.listen((event) {
      handleMessage(context, event);
    });
  }

  void handleMessage(BuildContext context, RemoteMessage message) async {
    if (message.data['type'] == 'message') {
      String customerId = message.data['customerId'];
      Customer? customer =
          await customerViewModel.fetchOtherCustomer(customerId);
      Navigator.push(
          context,
          MaterialPageRoute(
              builder: (context) => ChatPage(customer: customer!)));
    } else if (message.data['type'] == 'Job') {
      String jobId = message.data['itemId'];
      int parsedJobId = int.parse(jobId);
      Navigator.push(
          context,
          MaterialPageRoute(
              builder: (context) => UserJobDetailPage(
                    jobId: parsedJobId,
                  )));
    } else if (message.data['type'] == 'JobOffer') {
      String jobId = message.data['itemId'];
      int parsedJobId = int.parse(jobId);
      Navigator.push(
          context,
          MaterialPageRoute(
              builder: (context) => JobOfferDetailPage(
                    jobId: parsedJobId,
                    isActive: true,
                  )));
    } else if (message.data['type'] == 'NewJob') {
      Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => const MainPage(
              isBidder: true,
            ),
          ));
    } else {}
  }

  Future forgroundMessage() async {
    await FirebaseMessaging.instance
        .setForegroundNotificationPresentationOptions(
      alert: true,
      badge: true,
      sound: true,
    );
  }
}

关键问题代码段

if (Platform.isIOS) { 
  forgroundMessage();
} 

该分支仅调用了设置前台通知展示选项的方法,未像Android端那样初始化本地通知并调用showNotification,导致自定义声音无法触发。

main.dart代码

@pragma('vm:entry-point')
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
  SystemChrome.setSystemUIOverlayStyle(
      const SystemUiOverlayStyle(statusBarColor: Colors.transparent));
  runApp(const MyApp());
}

主页面initState调用代码

@override
void initState() {
  loginViewModel = Provider.of<LoginViewModel>(context, listen: false);
  notificationServices.requestNotificationPermission();
  notificationServices.forgroundMessage();
  notificationServices.firebaseInit(context);
  notificationServices.setupInteractMessage(context);
  notificationServices.isTokenRefresh();

  notificationServices.getDeviceToken().then((value) {
    if (kDebugMode) {
      print('device token');
      print(value);
    }
  });
  upBody = widget.isBidder ? const HomePageForBidder() : const HomePage();

  for (var tab in tabIconsList) {
    tab.isSelected = false;
  }
  tabIconsList[0].isSelected = true;

  animationController = AnimationController(
      duration: const Duration(milliseconds: 600), vsync: this);

  super.initState();
}

解决方案

修改firebaseInit函数,让iOS分支执行与Android端一致的本地通知初始化和展示逻辑:

void firebaseInit(BuildContext context) {
  FirebaseMessaging.onMessage.listen((message) {
    RemoteNotification? notification = message.notification;
    AndroidNotification? android = message.notification?.android;

    if (kDebugMode) {
      print("notifications title:${notification?.title}");
      print("notifications body:${notification?.body}");
      print('count:${android?.count}');
      print('data:${message.data.toString()}');
    }

    // 统一初始化本地通知并展示,覆盖iOS和Android
    initLocalNotifications(context, message);
    showNotification(message);

    // 保留iOS前台通知展示选项设置
    if (Platform.isIOS) {
      forgroundMessage();
    }
  });
}

额外检查项:

  1. 确认iOS项目的Runner/Resources目录下存在notification.wav文件,且文件名大小写与代码中一致(iOS对文件名大小写敏感)。
  2. 若需后台通知也播放自定义声音,FCM请求的payload中必须包含sound字段。
  3. 确保DarwinNotificationDetails中指定的声音文件名与实际文件完全匹配。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 08:45:56