支持推送通知
通过将 Firebase Cloud Messaging (FCM) 与 Singular SDK 集成,跟踪用户与推送通知的互动,以衡量再参与活动并准确归因于转化。
请遵循以下实施指南,以确保通知数据正确传递到 Singular SDK,从而获得正确的归因。
为什么要跟踪推送通知?推送通知能推动用户重新参与,但跟踪需要正确的整合。Singular 可确保与通知互动的用户得到正确归因,从而优化营销活动和参与策略。
实施指南
设置 Firebase 云消息
在 Flutter 应用程序中安装 Firebase 软件包并配置特定于平台的设置,以支持推送通知。
安装 Firebase 软件包
将 Firebase 依赖项添加到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 中配置推送通知功能。
- 注册 iOS 应用程序:在 Firebase 控制台项目中创建 iOS 应用程序
-
添加配置文件:下载
GoogleService-Info.plist并将其添加到 Xcode Runner 文件夹中 - 启用功能:在 Xcode 项目设置中,启用推送通知功能
- 启用后台模式:启用后台模式并勾选远程通知
安卓配置
在 Firebase 中注册您的 Android 应用程序,并将配置文件添加到您的项目中。
- 注册 Android 应用程序:在 Firebase 控制台项目中创建一个 Android 应用程序
-
添加配置文件:下载
google-services.json并将其放入android/app/ - 验证依赖关系:确保在 AndroidManifest.xml 中添加了 Firebase 消息传递依赖项并授予了权限
在 Flutter 中初始化 Firebase
配置 Firebase,使其在运行 Flutter 应用程序之前初始化,并为应用程序不在前台时收到的通知设置后台消息处理程序。
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 链接的关键路径。每个路径都是一个数组,代表键的嵌套结构。
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 以进行归因跟踪。
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.m 或AppDelegate.swift 中,将启动选项传递给 Singular SDK:
Objective-C 实现
// 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 实现
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 实现
// 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 实现
// 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 链接,请配置外部域。
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 应用程序特定平台处理程序的全面推送通知实施。
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 跟踪推送通知互动,从而提高活动性能洞察力,并确保准确的再参与归因。