Flutter SDK - プッシュ通知のサポート

ドキュメント

プッシュ通知のサポート

Firebase Cloud Messaging (FCM) とSingular SDKを統合することで、プッシュ通知によるユーザーインタラクションを追跡し、リエンゲージメントキャンペーンやコンバージョン率を正確に測定することができます。

以下の実装ガイドラインに従って、通知データが正しくSingular SDKに渡され、適切なアトリビューションが行われるようにしてください。

プッシュ通知を追跡する理由プッシュ通知はリエンゲージメントを促進しますが、トラッキングには正しい統合が必要です。Singularは、通知を受け取ったユーザーが適切にアトリビューションされるようにし、マーケティングキャンペーンとエンゲージメント戦略を最適化します。


実装ガイド

Firebaseクラウドメッセージングのセットアップ

Firebaseパッケージをインストールし、Flutterアプリケーションでプッシュ通知をサポートするためのプラットフォーム固有の設定を行います。

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の設定

iOSアプリをFirebaseに登録し、Xcodeでプッシュ通知機能を設定します。

  1. iOS アプリを登録します:Firebase Console プロジェクトで iOS アプリを作成します。
  2. 設定ファイルの追加: GoogleService-Info.plist をダウンロードし、Xcode Runner フォルダに追加します。
  3. 機能を有効にする:Xcode のプロジェクト設定で、Push Notifications 機能を有効にします。
  4. バックグラウンドモードを有効にする:バックグラウンドモードを有効にし、リモート通知をチェックする。

Androidの設定

AndroidアプリをFirebaseに登録し、設定ファイルをプロジェクトに追加します。

  1. Androidアプリを登録します:Firebase ConsoleプロジェクトにAndroidアプリを作成します。
  2. 設定ファイルの追加: google-services.json をダウンロードし、android/app/に配置します。
  3. 依存関係を確認します:AndroidManifest.xmlにFirebaseメッセージングの依存関係が追加され、パーミッションが付与されていることを確認する。

FlutterでFirebaseを初期化する

Flutterアプリを実行する前にFirebaseを初期化するように設定し、アプリがフォアグラウンドでないときに受信した通知のバックグラウンドメッセージハンドラを設定する。

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'] を使用します。
  • ネストされたキー:ネストした JSON 構造をトラバースするには['rootObj', 'nestedObj', 'key'] を使用する。
  • 複数のパス:複数のパス配列を定義して、Singularリンクのさまざまな可能性のある場所をチェックする。

設定プロパティ

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.m またはAppDelegate.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ネイティブの設定

アプリのバックグラウンドまたはフォアグラウンド

アプリがバックグラウンドまたはフォアグラウンド状態の時に、通知インテントをSingular SDKに渡すようにAndroid MainActivityを設定します。

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コンソールを使用して、プッシュ通知のトラッキングを確認します。ディープリンクURLフィールドをチェックして、トラッキングリンクが正しくキャプチャされていることを確認します。


高度な構成

ESPドメイン設定

SingularリンクをEメールサービスプロバイダ(ESP)またはその他のサードパーティのドメインでラップする場合は、外部ドメインを設定します。

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リファレンスを参照してください。


ダイナミック・ディープ・リンク・ルーティング

動的なリダイレクトオーバーライドで1つのSingularトラッキングリンクを設定することで、1つの通知から複数のディープリンク先を実装できます。

使用例複数のアクションオプションを持つニュース速報

  • 最新ニュースを読む newsapp://article?id=12345
  • トレンドトピック newsapp://trending
  • スポーツ newsapp://sports

複数のトラッキングリンクを作成する代わりに、1つのSingularリンクを使用し、ユーザーの選択に基づいて動的にリダイレクトを上書きします。実装の詳細については、Singular トラッキングリンクでリダイレクトを上書きするを参照してください。


完全な実装例

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の初期化によってトリガーされるセッション開始イベントに含めます。バックエンドはこのデータを処理し、プッシュ通知のタッチポイントをアトリビューションし、リエンゲージメント・トラッキングを登録します。
  • ドメインの制限デフォルトでは、[Manage Links(リンクの管理)]ページからの単一リンクドメイン(sng.link )のみが許可されます。ラップリンク用のESPドメインは、espDomainsを使用して明示的に設定します。
  • プラットフォームの違い:iOSでは終了状態のためにAppDelegateの設定が必要ですが、Androidではブリッジモジュールによって自動的に処理されます。
  • テスト:開発中にSDKロギングを有効にして、プッシュ通知データが正しくキャプチャされ、処理されていることを確認する。

成功:これらのステップに従うことで、あなたのアプリはSingularでプッシュ通知インタラクションを追跡し、キャンペーンパフォーマンスの洞察を向上させ、正確なリエンゲージメントアトリビューションを保証します。