Flutter SDK - 支持推送通知

文档

支持推送通知

通过将 Firebase Cloud Messaging (FCM) 与 Singular SDK 集成,跟踪用户与推送通知的互动,以衡量再参与活动并准确归因于转化。

请遵循以下实施指南,以确保通知数据正确传递到 Singular SDK,从而获得正确的归因。

为什么要跟踪推送通知?推送通知能推动用户重新参与,但跟踪需要正确的整合。Singular 可确保与通知互动的用户得到正确归因,从而优化营销活动和参与策略。


实施指南

设置 Firebase 云消息

在 Flutter 应用程序中安装 Firebase 软件包并配置特定于平台的设置,以支持推送通知。

安装 Firebase 软件包

将 Firebase 依赖项添加到pubspec.yaml 文件,以获得核心功能和消息支持。

pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  firebase_core: ^2.24.2
  firebase_messaging: ^14.7.10
  singular_flutter_sdk: ^1.8.0

添加依赖项后,运行flutter pub get 安装软件包。


iOS 配置

在 Firebase 中注册 iOS 应用程序,并在 Xcode 中配置推送通知功能。

  1. 注册 iOS 应用程序:在 Firebase 控制台项目中创建 iOS 应用程序
  2. 添加配置文件:下载GoogleService-Info.plist 并将其添加到 Xcode Runner 文件夹中
  3. 启用功能:在 Xcode 项目设置中,启用推送通知功能
  4. 启用后台模式:启用后台模式并勾选远程通知

安卓配置

在 Firebase 中注册您的 Android 应用程序,并将配置文件添加到您的项目中。

  1. 注册 Android 应用程序:在 Firebase 控制台项目中创建一个 Android 应用程序
  2. 添加配置文件:下载google-services.json 并将其放入android/app/
  3. 验证依赖关系:确保在 AndroidManifest.xml 中添加了 Firebase 消息传递依赖项并授予了权限

在 Flutter 中初始化 Firebase

配置 Firebase,使其在运行 Flutter 应用程序之前初始化,并为应用程序不在前台时收到的通知设置后台消息处理程序。

Dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';

// Background message handler (must be top-level function)
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  print('Background message: ${message.messageId}');
  print('Data: ${message.data}');
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Firebase
  await Firebase.initializeApp();

  // Set background message handler
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  runApp(MyApp());
}

配置推送链接路径

定义推送通知有效载荷结构中 Singular 跟踪链接所在的 JSON 路径。

通过传递字符串数组配置推送链接路径,字符串数组指定了通知数据结构中 Singular 链接的关键路径。每个路径都是一个数组,代表键的嵌套结构。

Dart
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';

SingularConfig config = SingularConfig(
  'YOUR_SDK_KEY',
  'YOUR_SDK_SECRET'
);

// Configure paths where Singular links are located in push payload
config.pushNotificationsLinkPaths = [
  ['sng_link'],                              // Top-level key
  ['path', 'to', 'url'],                     // Nested path
  ['rootObj', 'nestedObj', 'singularLink']   // Deep nested path
];

Singular.start(config);

路径配置示例

  • 简单键:对有效载荷中的顶级键使用['sng_link']
  • 嵌套键:使用['rootObj', 'nestedObj', 'key'] 遍历嵌套的 JSON 结构
  • 多路径:定义多个路径数组,以检查奇异链接的不同可能位置

配置属性

List<List<String>>? pushNotificationsLinkPaths

有关完整的配置文档,请参阅pushNotificationsLinkPaths 参考资料


特定平台处理

在 Flutter 中处理推送通知

实现 Firebase 消息监听器,以捕获前台和后台状态下的通知数据,然后将数据传递给 Singular 以进行归因跟踪。

Dart
import 'package:flutter/material.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:singular_flutter_sdk/singular.dart';

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

  @override
  void initState() {
    super.initState();
    setupPushNotifications();
  }

  void setupPushNotifications() async {
    // Request permission for iOS
    NotificationSettings settings = await _firebaseMessaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );

    print('User granted permission: ${settings.authorizationStatus}');

    // Handle foreground notifications
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print('Foreground message received: ${message.messageId}');
      handleForegroundNotification(message);
    });

    // Handle notifications that opened the app from background
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      print('Notification opened app from background: ${message.messageId}');
      handleBackgroundNotification(message);
    });

    // Check for notification that launched the app (terminated state)
    RemoteMessage? initialMessage = await _firebaseMessaging.getInitialMessage();
    if (initialMessage != null) {
      print('App launched from notification: ${initialMessage.messageId}');
      handleTerminatedNotification(initialMessage);
    }
  }

  void handleForegroundNotification(RemoteMessage message) {
    String title = message.notification?.title ?? '';
    String body = message.notification?.body ?? '';
    Map<String, dynamic> data = message.data;

    print('Title: $title');
    print('Body: $body');
    print('Data: $data');

    // Pass notification data to Singular
    if (data.isNotEmpty) {
      Singular.handlePushNotification(data);
    }

    // Display local notification or custom UI
    displayLocalNotification(title, body, data);
  }

  void handleBackgroundNotification(RemoteMessage message) {
    print('Processing background notification: ${message.messageId}');

    // Pass notification data to Singular
    if (message.data.isNotEmpty) {
      Singular.handlePushNotification(message.data);
    }

    // Navigate to specific screen based on notification data
    navigateFromNotification(message.data);
  }

  void handleTerminatedNotification(RemoteMessage message) {
    print('Processing terminated state notification: ${message.messageId}');

    // Pass notification data to Singular
    if (message.data.isNotEmpty) {
      Singular.handlePushNotification(message.data);
    }

    // Navigate to specific screen
    navigateFromNotification(message.data);
  }

  void displayLocalNotification(
    String title, 
    String body, 
    Map<String, dynamic> data
  ) {
    // Your notification display logic
    print('Displaying notification: $title - $body');
  }

  void navigateFromNotification(Map<String, dynamic> data) {
    // Your navigation logic based on notification data
    final route = data['route'];
    print('Navigating to: $route');
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      home: MyHomePage(),
    );
  }
}

方法签名

static void handlePushNotification(Map<String, dynamic> notificationData)

有关完整的方法文档,请参阅handlePushNotification 参考资料


iOS 本地配置

处于终止状态的应用程序

配置 iOS AppDelegate,将启动选项传递给 Singular SDK,以便在应用程序从终止状态打开时进行自动推送跟踪。

AppDelegate.mAppDelegate.swift 中,将启动选项传递给 Singular SDK:

Objective-C 实现

AppDelegate.m
// Import at the top of the file
#import "SingularAppDelegate.h"

- (BOOL)application:(UIApplication *)application 
    didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    [GeneratedPluginRegistrant registerWithRegistry:self];

    // Pass launch options to Singular for push tracking
    [SingularAppDelegate shared].launchOptions = launchOptions;

    return [super application:application 
        didFinishLaunchingWithOptions:launchOptions];
}

Swift 实现

AppDelegate.swift
import singular_flutter_sdk

override func application(_ application: UIApplication, 
    didFinishLaunchingWithOptions 
    launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    GeneratedPluginRegistrant.register(with: self)

    // Pass launch options to Singular for push tracking
    if let singularAppDelegate = SingularAppDelegate.shared() {
        singularAppDelegate.launchOptions = launchOptions
    }

    return super.application(application, 
        didFinishLaunchingWithOptions: launchOptions)
}

自动处理:当用户在应用程序未运行时点击推送通知时,Singular 会在应用程序启动时通过启动选项自动捕获通知有效载荷。


安卓本地配置

后台或前台应用程序

配置 Android MainActivity,以便在应用程序处于后台或前台状态时将通知意图传递给 Singular SDK。

在你的 MainActivity 中,覆盖onNewIntent ,将意图传递给 Singular:

Java 实现

MainActivity.java
// Add imports at the top
import android.content.Intent;
import com.singular.flutter_sdk.SingularBridge;

// Add to MainActivity class
@Override
public void onNewIntent(Intent intent) {
    if(intent.getData() != null) {
        setIntent(intent);
        super.onNewIntent(intent);

        // Pass intent to Singular for push tracking
        SingularBridge.onNewIntent(intent);
    }
}

Kotlin 实现

MainActivity.kt
// Add imports at the top
import android.content.Intent
import com.singular.flutter_sdk.SingularBridge

// Add to MainActivity class
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    if (intent.data != null) {
        setIntent(intent)

        // Pass intent to Singular for push tracking
        SingularBridge.onNewIntent(intent)
    }
}

应用程序处于终止状态

处于终止状态的 Android 应用程序无需额外配置。当用户点击通知时,Flutter 桥接层会自动处理这种情况。

自动处理:当用户在应用程序未运行时点击推送通知时,Singular 会通过原生桥接集成自动捕获通知数据。


验证指南

验证启动会话中的有效载荷

通过检查启动会话 API 调用,确认推送通知链接已正确传递给 Singular。

当用户点击通知时,Singular SDK 会在启动会话请求的singular_link 参数下包含推送通知有效载荷。

启动会话请求示例:

https://sdk-api-v1.singular.net/api/v1/start?
a=<SDK-Key>
&singular_link=https://singularassist2.sng.link/C4nw9/r1m0?_dl=singular%3A%2F%2Ftest&_smtype=3
&i=net.singular.sampleapp
&s=1740905574084
&sdk=Singular/Flutter-v1.8.0

替代验证:使用 Singular SDK 控制台验证推送通知跟踪。检查Deeplink URL字段,确认跟踪链接已正确捕获。


高级配置

ESP 域配置

如果您在电子邮件服务提供商(ESP)或其他第三方域中封装 Singular 链接,请配置外部域。

Dart
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';

// Configure ESP domains for wrapped Singular links
SingularConfig config = SingularConfig(
  'YOUR_SDK_KEY',
  'YOUR_SDK_SECRET'
);

config.espDomains = ['sl.esp.link', 'custom.domain.com'];

Singular.start(config);

配置属性

List<String>? espDomains

安全 注意:默认情况下,只允许使用 "Singular 管理链接 "页面中预定义的sng.link域。 如果使用封装链接,请明确配置 ESP 域。

有关完整的配置文档,请参阅espDomains 参考资料


动态深度链接路由

通过配置一个具有动态重定向重写功能的 Singular 跟踪链接,在单个通知中实现多个深度链接目的地。

用例示例:具有多个操作选项的突发新闻通知

  • 阅读最新消息: newsapp://article?id=12345
  • 热门话题 newsapp://trending
  • 体育 newsapp://sports

与其创建多个跟踪链接,不如使用一个单一链接,并根据用户选择动态覆盖重定向。有关实施详情,请参阅在奇异跟踪链接中覆盖重定向


完整实施示例

包含 Firebase 设置、Singular 配置和 Flutter 应用程序特定平台处理程序的全面推送通知实施。

Dart
import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';

// Background message handler (top-level function)
Future<void> _firebaseMessagingBackgroundHandler(RemoteMessage message) async {
  await Firebase.initializeApp();
  print('Background message: ${message.messageId}');

  // Singular handles background notifications automatically
  if (message.data.isNotEmpty) {
    print('Background notification data: ${message.data}');
  }
}

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // Initialize Firebase
  await Firebase.initializeApp();

  // Set background message handler
  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

  runApp(MyApp());
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
  final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
  final FirebaseMessaging _firebaseMessaging = FirebaseMessaging.instance;

  @override
  void initState() {
    super.initState();
    initializeSingular();
    setupPushNotifications();
  }

  void initializeSingular() {
    // Configure Singular SDK
    SingularConfig config = SingularConfig(
      'YOUR_SDK_KEY',
      'YOUR_SDK_SECRET'
    );

    // Configure push link paths
    config.pushNotificationsLinkPaths = [
      ['sng_link'],
      ['data', 'url'],
      ['rootObj', 'nestedObj', 'singularLink']
    ];

    // Configure ESP domains if needed
    config.espDomains = ['sl.esp.link'];

    // Enable logging for debugging
    config.enableLogging = true;

    // Initialize SDK
    Singular.start(config);
  }

  void setupPushNotifications() async {
    // Request permission (iOS)
    NotificationSettings settings = await _firebaseMessaging.requestPermission(
      alert: true,
      badge: true,
      sound: true,
    );

    if (settings.authorizationStatus == AuthorizationStatus.authorized) {
      print('User granted permission');
    }

    // Get FCM token
    String? token = await _firebaseMessaging.getToken();
    print('FCM Token: $token');

    // Handle foreground notifications
    FirebaseMessaging.onMessage.listen((RemoteMessage message) {
      print('Foreground notification: ${message.messageId}');
      handleForegroundNotification(message);
    });

    // Handle notification that opened app from background
    FirebaseMessaging.onMessageOpenedApp.listen((RemoteMessage message) {
      print('Notification opened app: ${message.messageId}');
      handleBackgroundNotification(message);
    });

    // Check for notification that launched the app
    RemoteMessage? initialMessage = await _firebaseMessaging.getInitialMessage();
    if (initialMessage != null) {
      print('App launched from notification: ${initialMessage.messageId}');
      handleTerminatedNotification(initialMessage);
    }
  }

  void handleForegroundNotification(RemoteMessage message) {
    final title = message.notification?.title ?? '';
    final body = message.notification?.body ?? '';
    final data = message.data;

    print('Foreground - Title: $title, Body: $body');

    // Pass notification data to Singular
    if (data.isNotEmpty) {
      Singular.handlePushNotification(data);
    }

    // Display notification to user
    showNotificationDialog(title, body, data);
  }

  void handleBackgroundNotification(RemoteMessage message) {
    print('Processing background notification');

    // Pass notification data to Singular
    if (message.data.isNotEmpty) {
      Singular.handlePushNotification(message.data);
    }

    // Navigate based on notification data
    navigateFromNotification(message.data);
  }

  void handleTerminatedNotification(RemoteMessage message) {
    print('Processing terminated state notification');

    // Pass notification data to Singular
    if (message.data.isNotEmpty) {
      Singular.handlePushNotification(message.data);
    }

    // Navigate based on notification data
    navigateFromNotification(message.data);
  }

  void showNotificationDialog(
    String title, 
    String body, 
    Map<String, dynamic> data
  ) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(title),
        content: Text(body),
        actions: [
          TextButton(
            onPressed: () {
              Navigator.of(context).pop();
              navigateFromNotification(data);
            },
            child: Text('Open'),
          ),
          TextButton(
            onPressed: () => Navigator.of(context).pop(),
            child: Text('Dismiss'),
          ),
        ],
      ),
    );
  }

  void navigateFromNotification(Map<String, dynamic> data) {
    // Parse notification data for routing
    final route = data['route'];
    final productId = data['product_id'];

    print('Navigating to: $route');

    // Navigate to appropriate screen
    if (route == 'product' && productId != null) {
      navigatorKey.currentState?.pushNamed('/product/$productId');
    } else if (route == 'promo') {
      navigatorKey.currentState?.pushNamed('/promo');
    } else {
      navigatorKey.currentState?.pushNamed('/');
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      navigatorKey: navigatorKey,
      initialRoute: '/',
      routes: {
        '/': (context) => HomeScreen(),
        '/product': (context) => ProductScreen(),
        '/promo': (context) => PromoScreen(),
      },
    );
  }
}

// Placeholder screens
class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Home')),
      body: Center(child: Text('Home Screen')),
    );
  }
}

class ProductScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Product')),
      body: Center(child: Text('Product Screen')),
    );
  }
}

class PromoScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Promo')),
      body: Center(child: Text('Promo Screen')),
    );
  }
}

重要注意事项

实施注意事项

  • 无回调处理程序:singularLinksHandler 不同,推送通知功能不提供有效负载回调。请实施您自己的深层链接逻辑,将用户导向您应用中的特定内容。
  • 归属流:当用户点击通知时,Singular 会检索有效载荷,并将其包含在 SDK 初始化触发的启动会话事件中。后台会处理这些数据,对推送通知接触点进行归因,并注册重新参与跟踪。
  • 域限制:默认情况下,只允许使用 "管理链接 "页面上的单链接域 (sng.link)。请使用espDomains为封装链接明确配置 ESP 域。
  • 平台差异:iOS 需要 AppDelegate 对终止状态进行配置,而 Android 则通过桥接模块自动进行处理
  • 测试:在开发过程中启用 SDK 日志,以验证推送通知数据是否被正确捕获和处理

成功:通过这些步骤,您的应用程序现在可以使用 Singular 跟踪推送通知互动,从而提高活动性能洞察力,并确保准确的再参与归因。