跳转到正文
YGJoon 技术博客

Flutter 推送通知实现详解

发布于:
·1.5k 字 · 约 4 分钟·作者:
FCM 拿 Token、requestPermission 要权限,flutter_local_notifications 做本地提醒:前台展示与点击跳转全覆盖
Flutter 推送通知实现详解文章封面
目录

简介

推送通知是移动应用的重要功能,可以让用户及时获取重要信息。本文介绍如何在 Flutter 中实现推送通知功能。

基本配置

添加依赖

dependencies:
  firebase_core: ^2.15.1
  firebase_messaging: ^14.6.7
  flutter_local_notifications: ^15.1.1

初始化 Firebase

Future<void> initFirebase() async {
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );
  
  // 获取 FCM Token
  final fcmToken = await FirebaseMessaging.instance.getToken();
  print('FCM Token: $fcmToken');
}

配置通知权限

Future<void> requestNotificationPermission() async {
  final messaging = FirebaseMessaging.instance;
  
  NotificationSettings settings = await messaging.requestPermission(
    alert: true,
    badge: true,
    sound: true,
  );
  
  print('Notification permission status: ${settings.authorizationStatus}');
}

本地通知

初始化本地通知

class NotificationService {
  static final NotificationService _instance = NotificationService._internal();
  factory NotificationService() => _instance;
  
  final FlutterLocalNotificationsPlugin _notifications = 
      FlutterLocalNotificationsPlugin();
      
  NotificationService._internal();
  
  Future<void> init() async {
    const androidSettings = AndroidInitializationSettings('@mipmap/ic_launcher');
    const iosSettings = DarwinInitializationSettings(
      requestAlertPermission: true,
      requestBadgePermission: true,
      requestSoundPermission: true,
    );
    
    const settings = InitializationSettings(
      android: androidSettings,
      iOS: iosSettings,
    );
    
    await _notifications.initialize(
      settings,
      onDidReceiveNotificationResponse: _onNotificationTapped,
    );
  }
  
  void _onNotificationTapped(NotificationResponse response) {
    // 处理通知点击事件
  }
  
  Future<void> showNotification({
    required String title,
    required String body,
    String? payload,
  }) async {
    const androidDetails = AndroidNotificationDetails(
      'default_channel',
      'Default Channel',
      importance: Importance.high,
      priority: Priority.high,
    );
    
    const iosDetails = DarwinNotificationDetails(
      presentAlert: true,
      presentBadge: true,
      presentSound: true,
    );
    
    const details = NotificationDetails(
      android: androidDetails,
      iOS: iosDetails,
    );
    
    await _notifications.show(
      0,
      title,
      body,
      details,
      payload: payload,
    );
  }
}

远程推送

处理后台消息

Future<void> _firebaseMessagingBackgroundHandler(
  RemoteMessage message,
) async {
  await Firebase.initializeApp();
  print('Handling background message: ${message.messageId}');
  
  // 显示本地通知
  await NotificationService().showNotification(
    title: message.notification?.title ?? '',
    body: message.notification?.body ?? '',
    payload: message.data.toString(),
  );
}

处理前台消息

void _handleForegroundMessage() {
  FirebaseMessaging.onMessage.listen((RemoteMessage message) {
    print('Got a message whilst in the foreground!');
    print('Message data: ${message.data}');

    if (message.notification != null) {
      print('Message also contained a notification:');
      print('Title: ${message.notification?.title}');
      print('Body: ${message.notification?.body}');
      
      NotificationService().showNotification(
        title: message.notification?.title ?? '',
        body: message.notification?.body ?? '',
        payload: message.data.toString(),
      );
    }
  });
}

完整示例

class PushNotificationDemo extends StatefulWidget {
  @override
  _PushNotificationDemoState createState() => _PushNotificationDemoState();
}

class _PushNotificationDemoState extends State<PushNotificationDemo> {
  final notificationService = NotificationService();
  String? _fcmToken;
  List<String> _notifications = [];
  
  @override
  void initState() {
    super.initState();
    _initPushNotifications();
  }
  
  Future<void> _initPushNotifications() async {
    // 初始化 Firebase
    await Firebase.initializeApp();
    
    // 初始化本地通知
    await notificationService.init();
    
    // 请求通知权限
    await requestNotificationPermission();
    
    // 获取 FCM Token
    final token = await FirebaseMessaging.instance.getToken();
    setState(() {
      _fcmToken = token;
    });
    
    // 监听 Token 刷新
    FirebaseMessaging.instance.onTokenRefresh.listen((token) {
      setState(() {
        _fcmToken = token;
      });
    });
    
    // 处理后台消息
    FirebaseMessaging.onBackgroundMessage(
      _firebaseMessagingBackgroundHandler,
    );
    
    // 处理前台消息
    FirebaseMessaging.onMessage.listen((message) {
      setState(() {
        _notifications.add(
          '${message.notification?.title}: ${message.notification?.body}',
        );
      });
      
      notificationService.showNotification(
        title: message.notification?.title ?? '',
        body: message.notification?.body ?? '',
      );
    });
    
    // 处理通知点击
    FirebaseMessaging.onMessageOpenedApp.listen((message) {
      print('Message clicked!');
      // 处理通知点击事件
    });
  }
  
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Push Notifications'),
      ),
      body: ListView(
        padding: EdgeInsets.all(16),
        children: [
          Card(
            child: Padding(
              padding: EdgeInsets.all(16),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    'FCM Token:',
                    style: TextStyle(fontWeight: FontWeight.bold),
                  ),
                  SizedBox(height: 8),
                  Text(_fcmToken ?? 'Loading...'),
                ],
              ),
            ),
          ),
          SizedBox(height: 16),
          Text(
            'Notifications:',
            style: TextStyle(
              fontSize: 18,
              fontWeight: FontWeight.bold,
            ),
          ),
          SizedBox(height: 8),
          ..._notifications.map((notification) => Card(
            child: ListTile(
              title: Text(notification),
            ),
          )).toList(),
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () {
          notificationService.showNotification(
            title: 'Test Notification',
            body: 'This is a test local notification',
          );
        },
        child: Icon(Icons.notifications),
      ),
    );
  }
}

最佳实践

  1. FCM Token 会变:除了登录时上报,务必监听 onTokenRefresh 实时同步到服务端;只在登录时上报一次,Token 一换服务端就推送不到人了——这是线上”收不到推送”最常见的根因。
  2. 通知分 channel(Android 8+ 强制要求):营销通知和重要通知(订单、验证码)分不同 channel,让用户能单独关掉营销类;全塞一个 channel,用户只能一刀切全关,重要通知也一起没了。
  3. 后台消息 handler 必须是顶层函数:写成类方法或闭包,App 被杀后系统调不起来;且 handler 里要重新 Firebase.initializeApp(文中代码已是这个做法)。

注意事项

  1. Android 13+(targetSdk 33+)通知权限变成运行时权限:不动态申请 POST_NOTIFICATIONS,通知直接静默不显示——上线前先看一眼自己的 targetSdk 是多少。
  2. 前台收到的 notification 消息不会自动弹横幅:必须自己调本地通知展示出来(文中 _handleForegroundMessage 已是这个做法);别以为接了 FCM,前台就有横幅。
  3. 国内 Android 机 FCM 基本不可达:只接 FCM 就上线,国内用户收不到推送;国内走小米/华为/OPPO/vivo 厂商通道、海外走 FCM,双通道是标配,不是加分项。
  4. 通知文案别写太长:Android 通知栏折叠显示,标题超过三四十个字会被截断——验证码、金额这类关键信息放最前面,别埋在句子中间。
  5. iOS 角标数字要自己清:服务端推一次 badge,数字不会自动归零;App 启动时调一次清角标,不然用户天天看着 99+ 的红点,强迫症都要犯了。

实战案例:定时通知的三个坑(这个场景我还没在生产环境跑过)

先说实话:推送通知这块我还没在自己项目里真正跑过生产环境,下面的坑是啃官方文档总结的,动手前建议先读一遍原文。第一个坑是 Android 14 的精确闹钟:从 Android 14 开始,定时通知想准时触发,得声明 SCHEDULE_EXACT_ALARM 权限并引导用户手动授权,否则系统会把你的定时任务”优化”掉。第二个坑是时区:定时通知必须用 timezone 包的 zonedSchedule + TZDateTime,直接传本地 DateTime,一到夏令时切换就全乱。第三个坑最要命:国产 ROM 的杀后台。本地定时通知本质上是”赌系统会叫醒你”,在部分激进机型上根本不可靠,重要提醒(比如订单、待办)必须有服务端推送做兜底。我的取舍思路是:纯本地提醒用 flutter_local_notifications 就够了;一旦通知内容依赖服务端状态,老老实实接 FCM/厂商通道,别在本地定时上死磕。

参考:flutter_local_notifications 官方文档(pub.dev)

总结

推送通知是提升用户活跃度的重要功能。通过合理使用 Firebase Cloud Messaging 和本地通知,可以实现完整的推送通知功能。注意在实现过程中要考虑平台差异和性能优化。


分享这篇文章:


相关文章

上一篇
Flutter 权限处理详解
下一篇
Flutter Scaffold 组件详解