Flutter艺术探索-Flutter推送通知:local_notifications与firebase_messaging

Flutter推送通知实战指南:如何用好local_notifications与firebase_messaging

引言

推送通知几乎是现代移动应用的标配,它能有效地提升用户活跃度和留存率。在Flutter里实现推送功能,通常会用到两个核心插件:local_notificationsfirebase_messaging。简单来说,前者负责在设备本地创建和显示通知,后者则帮你接收来自云端的消息。

网上基础的集成教程很多,但在实际项目中,要把推送做得稳定、体验好,需要考虑的细节远不止调用几个API。这篇文章就从一个实践者的角度,带你深入这两个插件,梳理清楚它们如何协同工作,并分享一套经过验证的、可以直接用在生产环境中的代码方案和避坑经验。

一、 核心原理:它们是如何工作的?

在动手写代码之前,先理解背后的运行机制,能帮你更快地定位后续可能遇到的问题。

1. local_notifications:你的本地通知管家

这个插件本身并不直接"画"出通知界面,它更像一个"协调员"。Flutter代码通过平台通道(Platform Channel) 调用它,它再转而调用Android或iOS的原生API去操作系统级的通知服务。

  • 在Android上 :插件会使用标准的NotificationManagerCompatNotificationCompat.Builder来创建通知。好处是你可以精细控制通知的样式,比如大图片、进度条和操作按钮,完全遵循Material Design规范。
  • 在iOS上 :插件则通过UNUserNotificationCenter来工作,从权限请求到创建通知(UNNotificationRequest),都贴合iOS的人机交互指南。
  • 它的特点很明确
    • 不依赖网络:由应用内部的逻辑触发,比如一个计时器结束、一项后台任务完成。
    • 即时显示:没有网络延迟,非常适合做闹钟、提醒这类功能。
    • 高度可定制:通知什么时候出现、长什么样、点击后干什么,都由你掌控。

2. firebase_messaging:连接云端的桥梁

这是Firebase Cloud Messaging (FCM) 的Flutter官方插件。FCM是Google提供的免费、可靠的跨平台消息推送服务。

  • 消息传递流程 : 你的应用服务器 → FCM服务器 → 用户设备上的原生FCM SDK → firebase_messaging插件 → 你的Flutter应用。
  • 关键在于消息的处理位置,这取决于应用的状态
    • 应用在前台 :消息会通过onMessage流直接传递到Flutter层,由你实时处理。
    • 应用在后台或完全被关闭:如果消息带有预定义的标题和正文(通知消息),系统会直接在通知栏显示。如果是纯数据消息,则需要你配置一个后台处理函数来接手。
  • 理解消息类型
    • 通知消息 :包含titlebody。应用在后台时,FCM会自动帮你展示,这是最省心的方式。
    • 数据消息:只包含自定义的键值对数据。无论应用在前台还是后台,都需要你自己写逻辑处理,适合静默同步数据。
    • 混合消息:同时包含上述两种负载。这种最灵活:后台时系统自动显示通知,前台时你又能拿到完整数据做定制化处理。

3. 为什么推荐两者结合使用?

在实际开发中,单独使用任何一个往往都不够:

  • firebase_messaging 做"接收器":专心负责从FCM接收各种云端推送指令。
  • local_notifications 做"显示器" :无论消息是从前台流来的,还是后台函数处理的,最终都统一用local_notifications来展示。这样做最大的好处是保证了通知样式和点击行为在全平台的一致性
  • 架构更清晰 :形成了FCM接收数据 → Flutter逻辑处理 → 本地通知展示的清晰流水线,代码解耦,便于测试和维护。

二、 从零开始:环境配置与集成

1. 添加依赖

首先,在项目的pubspec.yaml文件中引入必要的包。记得在pub.dev上核对一下最新版本。

yaml 复制代码
dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^2.24.2 # Firebase基础库,必须先初始化
  firebase_messaging: ^14.7.10
  flutter_local_notifications: ^16.2.0 # 注意包名是flutter_local_notifications

运行 flutter pub get 来安装它们。

2. 配置Firebase项目(Android & iOS)

这一步需要在Firebase控制台进行操作。

Android端配置:

  1. 创建Firebase项目(如果还没有)。

  2. 点击"添加应用",选择Android,输入你在android/app/build.gradle中设置的applicationId(包名)。

  3. 下载自动生成的google-services.json文件。

  4. 把这个文件放到你的Flutter项目目录:android/app/ 下。

  5. 在项目级的 android/build.gradle 文件里,确保有以下classpath:

    gradle 复制代码
    dependencies {
        // ... 其他classpath
        classpath 'com.google.gms:google-services:4.3.15' // 版本号可能有更新
    }
  6. 在模块级的 android/app/build.gradle 文件末尾,加上这行应用插件:

    gradle 复制代码
    apply plugin: 'com.google.gms.google-services'

iOS端配置:

  1. 在同一个Firebase项目中,再添加一个iOS应用,输入你的Bundle ID。

  2. 下载GoogleService-Info.plist文件。

  3. 用Xcode打开Flutter项目的ios文件夹。把刚才下载的.plist文件拖进Runner目录(记得勾选"Copy items if needed")。

  4. 在Xcode里,选中Runner目标,在 Signing & Capabilities 选项卡,点击 + Capability,添加 Push NotificationsBackground Modes。在Background Modes中,勾选 Remote notifications

  5. 打开(或创建)ios/Runner/AppDelegate.swift,在文件顶部导入Firebase,并在应用启动方法里初始化:

    swift 复制代码
    import UIKit
    import Flutter
    import Firebase // 添加这行
    
    @UIApplicationMain
    class AppDelegate: FlutterAppDelegate {
      override func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
      ) -> Bool {
        FirebaseApp.configure() // 添加这行
        GeneratedPluginRegistrant.register(with: self)
        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
      }
    }

3. iOS的额外设置:权限描述

为了在iOS上弹出通知权限请求,需要在ios/Runner/Info.plist中添加描述信息。同时,目前推荐让Flutter插件来处理消息,而非Firebase的代理。

xml 复制代码
<!-- 通知权限说明,会显示在系统弹窗上 -->
<key>NSUserNotificationsUsageDescription</key>
<string>我们希望向您发送重要的更新和提醒。</string>

<!-- 禁用Firebase的默认代理,让Flutter插件接管消息处理 -->
<key>FirebaseAppDelegateProxyEnabled</key>
<false/>

三、 代码实战:构建一个健壮的通知服务

我们把所有通知相关的逻辑封装到一个NotificationService类里,这样管理起来清晰,也方便复用。

1. 核心服务类 (notification_service.dart)

dart 复制代码
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter/material.dart';

class NotificationService {
  // 单例模式,全局一个实例就够了
  static final NotificationService _instance = NotificationService._internal();
  factory NotificationService() => _instance;
  NotificationService._internal();

  static final FlutterLocalNotificationsPlugin _localNotificationsPlugin = FlutterLocalNotificationsPlugin();
  static final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

  // 初始化本地通知插件
  Future<void> initLocalNotifications() async {
    const AndroidInitializationSettings androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher'); // 使用应用图标
    const DarwinInitializationSettings iosSettings = DarwinInitializationSettings(
      requestAlertPermission: false, // 我们在下面单独请求权限
      requestBadgePermission: true,
      requestSoundPermission: true,
    );
    const InitializationSettings initSettings = InitializationSettings(
      android: androidSettings,
      iOS: iosSettings,
    );

    await _localNotificationsPlugin.initialize(
      initSettings,
      onDidReceiveNotificationResponse: _onNotificationTap, // 设置通知点击回调
    );
  }

  // 初始化FCM并设置监听
  Future<void> initFCM() async {
    // 1. 请求通知权限(这会触发系统弹窗)
    NotificationSettings settings = await _firebaseMessaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
      provisional: false, // iOS 12+,设为true可以先发通知再要权限
    );
    print('用户授权状态: ${settings.authorizationStatus}');

    // 2. 获取设备的FCM Token,这个Token需要发给你的后端服务器,用于定向推送
    String? token = await _firebaseMessaging.getToken();
    print('FCM设备Token: $token');
    // TODO: 将这个token发送到你的应用服务器

    // 3. 监听Token刷新(用户重装应用等场景下Token会变)
    _firebaseMessaging.onTokenRefresh.listen((newToken) {
      print('FCM Token已更新: $newToken');
      // TODO: 将新token重新发送给你的服务器
    });

    // 4. 监听应用在前台时收到的消息
    FirebaseMessaging.onMessage.listen(_handleForegroundMessage);

    // 5. 设置后台消息处理函数(静态方法或顶层函数)
    FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  }

  // 处理前台收到的消息
  Future<void> _handleForegroundMessage(RemoteMessage message) async {
    print('收到前台消息: ${message.notification?.title}');
    // 即使用户正在使用App,我们也用本地通知显示出来,体验更好
    await showLocalNotification(
      id: message.hashCode,
      title: message.notification?.title ?? '新消息',
      body: message.notification?.body ?? '',
      payload: message.data['route'] ?? '/home', // 可以通过data传递跳转路由
    );
  }

  // 显示本地通知的通用方法
  Future<void> showLocalNotification({
    required int id,
    required String title,
    required String body,
    String? payload,
    String? channelId = 'high_importance_channel',
    String? channelName = '重要通知',
  }) async {
    // Android 8.0+ 需要创建通知渠道
    const AndroidNotificationDetails androidDetails = AndroidNotificationDetails(
      'high_importance_channel', // 渠道ID,与参数一致
      '重要通知',
      channelDescription: '此渠道用于接收重要的应用消息',
      importance: Importance.high,
      priority: Priority.high,
      showWhen: true,
    );
    // iOS通知设置
    const DarwinNotificationDetails iosDetails = DarwinNotificationDetails(
      presentAlert: true,
      presentBadge: true,
      presentSound: true,
    );
    // 合并平台配置
    const NotificationDetails platformDetails = NotificationDetails(
      android: androidDetails,
      iOS: iosDetails,
    );

    await _localNotificationsPlugin.show(
      id,
      title,
      body,
      platformDetails,
      payload: payload,
    );
  }

  // 处理通知被点击的事件
  void _onNotificationTap(NotificationResponse response) async {
    print('通知被点击,携带数据: ${response.payload}');
    // 这里可以根据payload进行页面跳转,例如:
    // Navigator.of(globalContext).pushNamed(response.payload ?? '/home');
    // 注意:需要能获取到全局的NavigatorState,可以通过GlobalKey或状态管理方案实现。
  }

  // 示例:安排一个未来的本地通知(比如提醒功能)
  Future<void> scheduleNotification() async {
    // 需要添加 timezone 包来处理时区
    await _localNotificationsPlugin.zonedSchedule(
      0,
      '计划好的提醒',
      '这是10秒后触发的本地通知',
      tz.TZDateTime.now(tz.local).add(const Duration(seconds: 10)),
      const NotificationDetails(
        android: AndroidNotificationDetails(
          'reminder_channel',
          '提醒',
          channelDescription: '用于计划任务和提醒',
        ),
      ),
      androidScheduleMode: AndroidScheduleMode.exactAllowWhileIdle, // 精确调度,即使设备处于低电量模式也尝试触发
      uiLocalNotificationDateInterpretation: UILocalNotificationDateInterpretation.absoluteTime,
    );
  }
}

// FCM后台消息处理函数(必须是静态方法或顶层函数)
@pragma('vm:entry-point') // 这个注解确保Dart在后台能定位到此函数
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  // 后台环境也需要初始化Firebase
  await Firebase.initializeApp();
  print('处理后台消息,ID: ${message.messageId}');

  // 通常在这里,我们使用local_notifications来显示通知
  final notification = message.notification;
  if (notification != null) {
    await NotificationService().showLocalNotification(
      id: message.hashCode,
      title: notification.title ?? '后台通知',
      body: notification.body ?? '',
      payload: message.data['route'],
    );
  }
  // 如果是纯数据消息,可以在这里执行一些逻辑,比如更新本地存储
}

2. 在应用启动时初始化 (main.dart)

dart 复制代码
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'notification_service.dart';

Future<void> main() async {
  // 初始化Flutter引擎绑定,并初始化Firebase
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp();

  // 初始化我们的通知服务
  final notificationService = NotificationService();
  await notificationService.initLocalNotifications();
  await notificationService.initFCM();

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: '推送通知Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      home: const NotificationDemoPage(),
    );
  }
}

3. 一个简单的演示页面 (notification_demo_page.dart)

dart 复制代码
import 'package:flutter/material.dart';
import 'notification_service.dart';

class NotificationDemoPage extends StatefulWidget {
  const NotificationDemoPage({super.key});

  @override
  State<NotificationDemoPage> createState() => _NotificationDemoPageState();
}

class _NotificationDemoPageState extends State<NotificationDemoPage> {
  final NotificationService _notificationService = NotificationService();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('推送通知演示')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            ElevatedButton(
              onPressed: () async {
                // 触发一个即时的本地通知
                await _notificationService.showLocalNotification(
                  id: DateTime.now().millisecondsSinceEpoch ~/ 1000, // 用时间戳生成一个简单ID
                  title: '本地通知测试',
                  body: '这是一条立即触发的本地通知',
                  payload: '/detail', // 点击后可以跳转到详情页
                );
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('本地通知已触发')),
                );
              },
              child: const Text('触发本地通知'),
            ),
            const SizedBox(height: 20),
            ElevatedButton(
              onPressed: () {
                // 设置一个10秒后的计划通知
                _notificationService.scheduleNotification();
                ScaffoldMessenger.of(context).showSnackBar(
                  const SnackBar(content: Text('计划通知已设置 (10秒后)')),
                );
              },
              child: const Text('设置计划通知 (10秒后)'),
            ),
            const SizedBox(height: 20),
            const Padding(
              padding: EdgeInsets.all(20.0),
              child: Text('你还可以通过Firebase控制台的"云消息传递"功能,向所有设备发送测试推送消息,来验证FCM集成是否成功。', textAlign: TextAlign.center,),
            ),
          ],
        ),
      ),
    );
  }
}

四、 让推送功能更专业:优化与最佳实践

把功能跑通只是第一步,要让推送真正好用、不招人烦,还需要注意下面几点:

  1. 优化后台消息处理

    • _firebaseMessagingBackgroundHandler 函数要尽量轻快。避免在里面执行耗时操作(如大文件下载、复杂计算),否则可能会被系统终止。
    • 如果真有重活要干,可以考虑使用Isolate,但要处理好与主Isolate的通信。
  2. 管理好通知渠道 (Android)

    • 为不同类型的通知创建独立的渠道(如"私信"、"系统通知"、"促销活动")。用户可以在手机系统设置里对不同渠道进行开关,体验更好。
    • 慎重设置渠道的"重要性" (importance),别把所有通知都设成"高重要性",那会惹恼用户的。
  3. 防止通知"轰炸"

    • 对连续的同类型消息(比如同一条新闻的多次更新),可以考虑使用相同的通知id进行更新,而不是创建一堆新通知。
    • 在服务端或客户端对非紧急的高频消息做一下节流。
  4. 考虑电量和流量

    • FCM本身已经做了很多优化。你的服务器应该避免在用户深夜休息时发送促销通知。
    • 对于纯数据同步类的静默推送,可以考虑在设备连接Wi-Fi时再批量发送。
  5. 打磨用户体验细节

    • 点击要有用:确保每条通知点击后都能跳转到正确的页面,并且能还原当时的上下文。别让用户点进去一头雾水。
    • 增加快捷操作 :利用local_notifications插件,在通知上添加"回复"、"标记已读"等按钮,让用户不用打开App就能完成简单操作。
    • 优雅地请求权限:不要在用户一打开App时就突然弹出权限请求。可以先通过页面文案解释通知的价值(比如"接收订单状态提醒"),再引导用户开启,授权率会高很多。

五、 调试与常见问题

遇到推送收不到或者不正常?可以按照以下思路排查:

  • Android调试 :在电脑上使用 adb logcat 命令查看设备日志。可以过滤 FlutterLocalNotificationsPluginFirebaseMessaging 等关键字来缩小范围。
  • iOS调试 :一定要在真机上测试推送,模拟器不行。通过Xcode的"Console"查看设备运行日志。
  • 最快的FCM集成验证:直接去Firebase控制台的"云消息传递"页面,填写标题和内容,发送测试消息。如果收到了,说明FCM基础通道是通的。
  • 几个常见坑
    • iOS死活收不到远程推送:检查APNs证书(或认证密钥)是否正确上传到了Firebase控制台,以及Bundle ID是否完全匹配。
    • Android后台通知不显示 :确认 onBackgroundMessage 设置的回调函数是顶层或静态函数 ,并且 @pragma('vm:entry-point') 注解已添加。
    • 通知点击没反应 :检查 initialize 方法里的 onDidReceiveNotificationResponse 回调是否设置成功,以及显示通知时传递的 payload 是否被正确携带到点击回调中。

写在最后

通过上面的步骤,我们搭建了一个结合 firebase_messaginglocal_notifications 的Flutter推送体系。前者作为可靠的云端入口,后者提供一致且可控的本地展示,这种组合在实践中非常有效。

记住,推送通知是把双刃剑。用得好,它是激活用户、传递价值的利器;用不好,频繁打扰或推送无关内容,用户会毫不犹豫地关闭权限甚至卸载应用。始终从用户的角度出发,提供有意义、有温度的通知,才是长久之道。

希望这篇结合实践的文章能帮你避开一些坑,更顺畅地实现Flutter的推送功能。如果在实践中遇到新问题,欢迎分享和讨论。

相关推荐
2601_949809592 小时前
flutter_for_openharmony家庭相册app实战+隐私设置实现
android·javascript·flutter
灰灰勇闯IT2 小时前
Flutter for OpenHarmony:进度条与加载指示器 —— 构建流畅、可感知的异步交互体验
flutter·交互
灰灰勇闯IT2 小时前
Flutter for OpenHarmony:下拉刷新(RefreshIndicator)—— 构建即时、可信的数据同步体验
flutter·华为·交互
雨季6662 小时前
Flutter 三端应用实战:OpenHarmony “极简文本字符计数器”——量化表达的尺度
开发语言·flutter·ui·交互·dart
小哥Mark2 小时前
Flutter无状态和有状态组件在鸿蒙应用程序中的实战示例
flutter·华为·harmonyos
小哥Mark2 小时前
Flutter下拉刷新和滚动条组件在鸿蒙应用程序实战示例
flutter·华为·harmonyos
晚霞的不甘2 小时前
Flutter for OpenHarmony 实现 iOS 风格科学计算器:从 UI 到表达式求值的完整解析
前端·flutter·ui·ios·前端框架·交互
雨季6662 小时前
Flutter 三端应用实战:OpenHarmony “呼吸灯”——在焦虑时代守护每一次呼吸的数字禅修
开发语言·前端·flutter·ui·交互
2601_949543012 小时前
Flutter for OpenHarmony垃圾分类指南App实战:资讯详情实现
android·java·flutter