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

Flutter闹钟通知自定义铃声不播放、操作触发异常的问题求助

Flutter闹钟通知自定义铃声不播放、操作触发异常的问题求助

我目前在开发一个Flutter闹钟应用,用flutter_local_notifications做通知调度,想实现触发时播放自定义铃声,还加了「Stop Alarm」按钮来停止铃声。现在遇到几个棘手的问题:

  • 通知能按时弹出,但Android上的铃声不是系统闹钟那种强制播放的效果(要么音量小要么没响)
  • iOS端完全听不到铃声
  • 点击通知有时候不会触发playAlarmSound()方法
  • 不确定自己的音频文件放置位置和配置是否正确

以下是我写的NotificationService代码:

import 'dart:async';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:timezone/data/latest_all.dart' as tz;
import 'package:timezone/timezone.dart' as tz;
import 'package:audioplayers/audioplayers.dart';

class NotificationService {
  static final FlutterLocalNotificationsPlugin _notificationsPlugin =
      FlutterLocalNotificationsPlugin();
  static final AudioPlayer _audioPlayer = AudioPlayer();
  static bool _isAlarmPlaying = false;

  static Future<void> init() async {
    const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
    const iosSettings = DarwinInitializationSettings();
    const initSettings = InitializationSettings(
      android: androidSettings,
      iOS: iosSettings,
    );

    await _notificationsPlugin.initialize(
      initSettings,
      onDidReceiveNotificationResponse: (details) {
        if (details.payload == 'alarm_trigger') {
          playAlarmSound();
        }
        if (details.actionId == 'stop_alarm_action') {
          stopAlarmSound();
        }
      },
    );

    // iOS Permissions
    await _notificationsPlugin
        .resolvePlatformSpecificImplementation<
            IOSFlutterLocalNotificationsPlugin>()
        ?.requestPermissions(alert: true, sound: true, badge: true);

    tz.initializeTimeZones();
  }

  static Future<void> scheduleAlarm({
    required int id,
    required String title,
    required String body,
    required DateTime scheduledTime,
  }) async {
    final tzScheduledTime = tz.TZDateTime.from(scheduledTime, tz.local);

    const androidDetails = AndroidNotificationDetails(
      'alarm_channel',
      'Alarm Notifications',
      channelDescription: 'Used for daily alarms',
      importance: Importance.max,
      priority: Priority.high,
      playSound: true,
      fullScreenIntent: true,
      sound: RawResourceAndroidNotificationSound('alarm_sound'),
      actions: <AndroidNotificationAction>[
        AndroidNotificationAction(
          'stop_alarm_action',
          'Stop Alarm',
          showsUserInterface: true,
          cancelNotification: true,
        ),
      ],
    );

    const iosDetails = DarwinNotificationDetails(
      sound: 'alarm_sound.mp3',
      presentSound: true,
    );

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

    await _notificationsPlugin.zonedSchedule(
      id,
      title,
      body,
      tzScheduledTime,
      notificationDetails,
      androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle,
      payload: 'alarm_trigger',
    );
  }

  static Future<void> cancelAlarm(int id) async {
    await _notificationsPlugin.cancel(id);
  }

  static Future<void> playAlarmSound() async {
    if (!_isAlarmPlaying) {
      _isAlarmPlaying = true;
      try {
        await _audioPlayer.play(AssetSource('sounds/alarm_sound.mp3'));
      } catch (e) {
        print('Error playing alarm sound: $e');
      }
    }
  }

  static Future<void> stopAlarmSound() async {
    if (_isAlarmPlaying) {
      await _audioPlayer.stop();
      _isAlarmPlaying = false;
    }
  }

  static Future<void> scheduleDailyReflectionReminder({
    required int hour,
    required int minute,
  }) async {
    final now = DateTime.now();
    final scheduledTime = DateTime(
      now.year,
      now.month,
      now.day,
      hour,
      minute,
    );
    final finalScheduledTime = scheduledTime.isBefore(now)
        ? scheduledTime.add(const Duration(days: 1))
        : scheduledTime;

    await scheduleAlarm(
      id: 1001,
      title: 'Time for Reflection',
      body: 'Take a moment to reflect, pray, or review your day.',
      scheduledTime: finalScheduledTime,
    );
  }

  static Future<void> cancelDailyReflectionReminder() async {
    await cancelAlarm(1001);
  }
}

针对问题的排查与解决建议

一、音频文件放置与基础配置检查

Android端
  1. 自定义铃声必须放在android/app/src/main/res/raw/目录下,文件名要全小写、无特殊字符(比如你的alarm_sound.mp3要严格对应)
  2. 想让铃声像系统闹钟一样强制高音量播放,需要补充权限和音频流配置:
    • 在AndroidManifest.xml添加权限:
      <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
      <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK" />
      
    • 修改playAlarmSound(),切换到闹钟音频流并设置循环播放:
      static Future<void> playAlarmSound() async {
        if (!_isAlarmPlaying) {
          _isAlarmPlaying = true;
          try {
            await _audioPlayer.setReleaseMode(ReleaseMode.loop);
            if (Platform.isAndroid) {
              await _audioPlayer.setAudioContext(const AudioContext(
                android: AndroidAudioUsage.alarm,
              ));
            }
            await _audioPlayer.play(AssetSource('sounds/alarm_sound.mp3'));
          } catch (e) {
            print('Error playing alarm sound: $e');
          }
        }
      }
      
iOS端
  1. 音频文件要放在ios/Runner/Resources/目录,并且必须通过Xcode添加到项目(右键Runner → Add Files to "Runner",勾选「Copy items if needed」和目标Runner)
  2. 在Info.plist添加后台音频权限:
    <key>UIBackgroundModes</key>
    <array>
        <string>audio</string>
    </array>
    <key>NSAppleMusicUsageDescription</key>
    <string>需要播放闹钟铃声</string>
    
  3. 补充iOS的通知Action配置:
    const iosDetails = DarwinNotificationDetails(
      sound: 'alarm_sound.mp3',
      presentSound: true,
      actions: <DarwinNotificationAction>[
        DarwinNotificationAction(
          'stop_alarm_action',
          'Stop Alarm',
          options: <DarwinNotificationActionOption>{},
        ),
      ],
    );
    

二、通知点击与Action触发异常修复

  1. 后台回调处理:Android的全屏意图可能触发后台回调,需要在初始化时补充onDidReceiveBackgroundNotificationResponse:
    await _notificationsPlugin.initialize(
      initSettings,
      onDidReceiveNotificationResponse: (details) { ... },
      onDidReceiveBackgroundNotificationResponse: (details) {
        if (details.payload == 'alarm_trigger') {
          playAlarmSound();
        }
        if (details.actionId == 'stop_alarm_action') {
          stopAlarmSound();
        }
      },
    );
    
  2. 调整Android Action的showsUserInterface:如果不需要拉起应用,设为false,避免影响后台停止逻辑:
    AndroidNotificationAction(
      'stop_alarm_action',
      'Stop Alarm',
      showsUserInterface: false,
      cancelNotification: true,
    )
    

三、播放逻辑健壮性优化

  1. 停止铃声时释放播放器资源,避免内存泄漏:
    static Future<void> stopAlarmSound() async {
      if (_isAlarmPlaying) {
        await _audioPlayer.stop();
        await _audioPlayer.release();
        _isAlarmPlaying = false;
      }
    }
    
  2. 用SharedPreferences持久化播放状态,避免应用重启后状态丢失:
    (可以在init()中读取保存的状态,更新_isAlarmPlaying)

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.08 14:53:00