Flutter SDK - ディープリンクのサポート

ドキュメント

ディープリンク機能の追加

ディープリンクは、アプリ内の特定のコンテンツへユーザーを誘導します。アプリがインストールされた端末でディープリンクをタップすると、商品ページや特定の体験など、意図したコンテンツに直接アプリが開きます。

Singularトラッキングリンクは、標準ディープリンク(インストール済みアプリ向け)と遅延ディープリンク(新規インストール向け)の両方をサポートします。詳細については、ディープリンクFAQおよび SingularリンクFAQを参照してください。


要件

前提条件

アプリのディープリンクを有効化するには、Singularリンクの前提条件を完了してください。

注記:

  • 本記事は、組織がSingular Links(Singularのトラッキングリンク技術)を利用していることを前提としています。従来型のトラッキングリンクを使用しているお客様もいらっしゃる可能性があります。
  • アプリのディープリンク先は、Singularの「Apps」ページで設定する必要があります(アプリのアトリビューション追跡設定を参照)。

利用可能なパラメータ

SingularLinkハンドラーは、アプリ起動時にSingularトラッキングリンクからディープリンク、遅延ディープリンク、パススルーパラメータへのアクセスを提供します。

  • ディープリンク(_dl):リンクをクリックしたユーザー向けのアプリ内到達先URL
  • 遅延ディープリンク(_ddl):リンククリック後にアプリをインストールしたユーザー向けのリダイレクト先URL
  • パススルー (_p):追加コンテキスト用のトラッキングリンク経由で渡されるカスタムデータ

Flutter 3.35では、ディープリンクの処理方法に互換性のない変更が導入されました。 内部のdidPushRouteInformation() 関数が、サードパーティSDKが処理する前に ディープリンクをインターセプトしてデコードするようになりました。これにより Singularのディープリンクハンドラーとの競合が発生し、Singularリンクが 未知のルートとして扱われ、意図した目的地ではなくホーム画面に ユーザーが誘導される問題が生じます。

これはAndroidとiOSの両方に影響します。アプリが Flutter 3.35以降を対象としている場合、標準的な ディープリンク実装を進める前に、以下のプラットフォーム固有の 追加設定を完了してください。

注:これは Flutter フレームワークレベルの変更であり、 Singular SDK のバグではありません。SDK の更新は不要です。以下の設定により、 すべてのアプリ状態(フォアグラウンド、バックグラウンド、コールドスタート)で 正しいディープリンクルーティング動作が復元されます。

Androidでは2つの変更が必要です:Flutterの組み込みディープリンクハンドラーを無効化し、 インテントをFlutterエンジンとSingular SDKの両方に手動で転送すること。

AndroidManifest.xml ファイルの<activity> タグ内に、以下の<meta-data> エントリを追加します:

AndroidManifest.xml
<meta-data
    android:name="flutter_deeplinking_enabled"
    android:value="false" />

MainActivity を更新し、FlutterエンジンとSingular SDKの両方に インテントを転送するようにします。

MainActivity.kt MainActivity.java
// Add as part of the imports at the top of the class
import android.content.Intent
import com.singular.flutter_sdk.SingularBridge

// Add to the MainActivity class
override fun onNewIntent(intent: Intent) {
    super.onNewIntent(intent)
    setIntent(intent)

    // Forward to Flutter engine
    flutterEngine?.activityControlSurface?.onNewIntent(intent)

    // Forward to Singular SDK
    if (intent.data != null) {
        SingularBridge.onNewIntent(intent)
    }
}
// Add as part of the imports at the top of the class
import android.content.Intent;
import com.singular.flutter_sdk.SingularBridge;

// Add to the MainActivity class
@Override
public void onNewIntent(Intent intent) {
    super.onNewIntent(intent);
    setIntent(intent);

    // Forward to Flutter engine
    if (getFlutterEngine() != null) {
        getFlutterEngine().getActivityControlSurface().onNewIntent(intent);
    }

    // Forward to Singular SDK
    if (intent.getData() != null) {
        SingularBridge.onNewIntent(intent);
    }
}

注:これは、以下の Android プラットフォーム設定セクションで示されている Flutter 3.35+ プロジェクト向けの標準的な onNewIntent 実装に取って代わるものです。Flutter 3.10.x 以前の場合は、 代わりに標準実装を使用してください。

アプリのInfo.plist で、FlutterDeepLinkingEnabled フラグをfalse に設定し、 Flutterのネイティブディープリンクインターセプトを無効化します:

Info.plist
<key>FlutterDeepLinkingEnabled</key>
<false/>

注:このフラグ以外に AppDelegate への追加変更は不要です。 下記の「iOSプラットフォーム設定」セクションで示す 標準的なiOS AppDelegate設定を続けてください。


プラットフォーム設定

iOSプラットフォーム設定

iOS AppDelegateの更新

Singular SDKが起動関連データを処理しディープリンクを管理できるよう、AppDelegate ファイル内で Singular SDKにlaunchOptions およびuserActivity オブジェクトを渡します。

これらのオブジェクトには、アプリの起動方法や起動理由に関する重要な情報が含まれており、 Singularはこれをアトリビューション追跡やディープリンクナビゲーションに利用します。

Objective-C実装

AppDelegate.m
// Top of AppDelegate.m
#import "SingularAppDelegate.h"

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

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

- (BOOL)application:(UIApplication *)application 
    continueUserActivity:(NSUserActivity *)userActivity 
    restorationHandler:(void (^)(NSArray<id<UIUserActivityRestoring>> 
        *restorableObjects))restorationHandler {
    [[SingularAppDelegate shared] continueUserActivity:userActivity 
        restorationHandler:restorationHandler];

    return [super application:application 
        continueUserActivity:userActivity 
        restorationHandler:restorationHandler];
}

- (BOOL)application:(UIApplication *)app 
    openURL:(NSURL *)url 
    options:(NSDictionary<UIApplicationOpenURLOptionsKey,id> *)options {
    [[SingularAppDelegate shared] handleOpenUrl:url options:options];

    return [super application:app openURL:url options:options];
}

Swift実装

AppDelegate.swift
import singular_flutter_sdk

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

    if let singularAppDelegate = SingularAppDelegate.shared() {
        singularAppDelegate.launchOptions = launchOptions
    }

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

override func application(_ application: UIApplication, 
    continue userActivity: NSUserActivity, 
    restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
    if let singularAppDelegate = SingularAppDelegate.shared() {
        singularAppDelegate.continueUserActivity(userActivity, 
            restorationHandler: nil)
    }

    return super.application(application, 
        continue: userActivity, 
        restorationHandler: restorationHandler)
}

override func application(_ app: UIApplication, 
    open url: URL, 
    options: [UIApplication.OpenURLOptionsKey : Any] = [:]) -> Bool {
    if let singularAppDelegate = SingularAppDelegate.shared() {
        singularAppDelegate.handleOpen(url, options: options)
    }

    return super.application(app, open: url, options: options)
}

Androidプラットフォームの設定

Android MainActivityの更新

MainActivity ファイルを修正し、Intent オブジェクトをSingular SDKに渡すことで、起動関連データの処理とディープリンクの取り扱いを有効にします。

Intent オブジェクトには、アプリの起動方法と起動理由に関する情報が含まれており、 Singularはこれをアトリビューション追跡とディープリンクナビゲーションに使用します。

Java実装

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

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

Kotlin実装

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

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

SDK設定

Singularリンクハンドラーの実装

SDK初期化時にsingularLinksHandler コールバックを設定し、 受信するディープリンクおよび遅延ディープリンクデータを処理します。

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

void main() {
  runApp(MyApp());
}

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

class _MyAppState extends State<MyApp> {
  @override
  void initState() {
    super.initState();
    initializeSDK();
  }

  void initializeSDK() {
    // Create SDK configuration
    SingularConfig config = SingularConfig(
      'YOUR_SDK_KEY',
      'YOUR_SDK_SECRET'
    );

    // Configure deep link handler
    config.singularLinksHandler = (SingularLinkParams params) {
      print('=== Singular Link Resolved ===');
      print('Deep link: ${params.deeplink}');
      print('Passthrough: ${params.passthrough}');
      print('Is deferred: ${params.isDeferred}');

      // Handle deep link navigation
      if (params.deeplink != null) {
        handleDeepLink(params.deeplink!, params.isDeferred);
      }

      // Handle passthrough data
      if (params.passthrough != null) {
        handlePassthroughData(params.passthrough!);
      }
    };

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

  void handleDeepLink(String url, bool isDeferred) {
    print('Routing to: $url (Deferred: $isDeferred)');

    // Parse URL and navigate to appropriate screen
    // Example: myapp://product/123
    if (url.contains('product')) {
      final productId = url.split('/').last;
      navigateToProduct(productId);
    } else if (url.contains('promo')) {
      navigateToPromo(url);
    }
  }

  void handlePassthroughData(String passthrough) {
    print('Processing passthrough data: $passthrough');
    // Parse and use passthrough data as needed
  }

  void navigateToProduct(String productId) {
    // Your navigation logic
    print('Navigating to product: $productId');
  }

  void navigateToPromo(String url) {
    // Your navigation logic
    print('Navigating to promo: $url');
  }

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

設定プロパティ:

void Function(SingularLinkParams)? singularLinksHandler

注: singularLinksHandler コールバックは、 アプリがSingularリンク経由で起動した場合にのみトリガーされます。詳細は Singular Links FAQを参照してください。

完全なドキュメントについては、 singularLinksHandler リファレンスを参照してください。


ハンドラーの動作

リンク解決の理解

singularLinksHandler の動作は、アプリが新規インストール時か既存インストール時かによって異なります。

新規インストール時(遅延ディープリンク)

新規インストール時、アプリ起動時には Open URL が存在しません。Singular はアトリビューションを完了し、トラッキングリンクにディープリンクまたは遅延ディープリンクの値が含まれていたかを判断します。

遅延ディープリンクのフロー:

  1. ユーザーがディープリンク値を設定したSingularトラッキングリンクをクリック
  2. ユーザーがアプリをインストールし初回起動
  3. Singular SDKが最初のセッションをSingularサーバーに送信
  4. アトリビューションが完了し、トラッキングリンクからディープリンクを特定
  5. deeplink ディープリンク値がisDeferred = trueパラメータでsingularLinksHandler コールバックに返される( を含む)

遅延ディープリンクのテスト:

  1. テスト端末からアプリをアンインストール(現在インストールされている場合)
  2. iOS:IDFAをリセットしてください。Android:Google広告ID (GAID) をリセットしてください
  3. デバイスからSingularトラッキングリンクをクリック(ディープリンク値が設定されていることを確認)
  4. アプリをインストールして起動する

アトリビューションが正常に完了し、遅延ディープリンクの値がsingularLinksHandler ハンドラーに渡されます。

プロのヒント:異なるパッケージ名(例:com.example.prod ではなくcom.example.dev )を使用した開発ビルドでディープリンクをテストする場合、開発アプリのパッケージ名に合わせてトラッキングリンクを特別に設定してください。テストリンクをクリックした後、アプリストアから本番アプリをダウンロードするのではなく、Android StudioまたはXcode経由で開発ビルドを直接デバイスにインストールしてください。


既にインストール済み(即時ディープリンク)

アプリが既にインストールされている場合、Singularリンクをクリックすると、ユニバーサルリンク(iOS)またはAndroidアプリリンク技術を使用してアプリが即座に起動します。

即時ディープリンクのフロー:

  1. ユーザーがSingularトラッキングリンクをクリック
  2. OSがSingularトラッキングリンク全体を含むOpen URLを提供
  3. SDK初期化時にSingularがURLを解析
  4. Singularがdeeplinkpassthrough の値を抽出
  5. 値はsingularLinksHandler ハンドラー経由でisDeferred = falseと共に返される

高度な機能

パススルーパラメータ

パススルーパラメータを使用して、トラッキングリンクのクリックから追加データを取得します。 パラメータ。

トラッキングリンクにpassthrough (_p) パラメータが含まれている場合、singularLinksHandler ハンドラのpassthrough パラメータに対応するデータが含まれます。 キャンペーンメタデータ、ユーザーセグメンテーションデータ、 アプリ内で必要なカスタム情報の取得に利用します。

Dart
import 'package:singular_flutter_sdk/singular_config.dart';
import 'package:singular_flutter_sdk/singular_link_params.dart';
import 'dart:convert';

SingularConfig config = SingularConfig('API_KEY', 'SECRET');

config.singularLinksHandler = (SingularLinkParams params) {
  // Extract passthrough data
  final passthroughData = params.passthrough;

  if (passthroughData != null) {
    try {
      // Parse JSON passthrough data
      final jsonData = jsonDecode(passthroughData);

      print('Campaign ID: ${jsonData['campaign_id']}');
      print('User Segment: ${jsonData['segment']}');
      print('Promo Code: ${jsonData['promo_code']}');

      // Apply campaign-specific settings
      applyCampaignSettings(jsonData);
    } catch (error) {
      print('Error parsing passthrough data: $error');
    }
  }
};

void applyCampaignSettings(Map<String, dynamic> data) {
  // Your campaign logic here
  print('Applying campaign settings: $data');
}

すべてのクエリパラメータを転送

トラッキングリンクURLからすべてのクエリパラメータを取得するには、トラッキングリンクに_forward_params=2 パラメータを追加します。

_forward_params=2 をトラッキングリンクに追加すると、すべてのクエリパラメータがsingularLinksHandler ハンドラのdeeplink パラメータに含まれ、すべてのパラメータを含む完全な URL にアクセスできるようになります。

トラッキングリンクの例:
https://yourapp.sng.link/A1b2c/abc123?_dl=myapp://product/123&_forward_params=2&utm_source=facebook&promo=SALE2024

singularLinksHandler ハンドラーが受け取る内容:
deeplink = "myapp://product/123?utm_source=facebook&promo=SALE2024"


完全実装例

Flutterアプリ向けのナビゲーション、パススルー処理、パラメータ転送を包括的に実装したディープリンク。

Dart
import 'package:flutter/material.dart';
import 'package:singular_flutter_sdk/singular.dart';
import 'package:singular_flutter_sdk/singular_config.dart';
import 'package:singular_flutter_sdk/singular_link_params.dart';
import 'dart:convert';

void main() {
  runApp(MyApp());
}

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

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

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

  void initializeSDK() {
    SingularConfig config = SingularConfig(
      'YOUR_SDK_KEY',
      'YOUR_SDK_SECRET'
    );

    config.singularLinksHandler = (SingularLinkParams params) {
      print('=== Singular Link Resolved ===');
      print('Deep link: ${params.deeplink}');
      print('Passthrough: ${params.passthrough}');
      print('Is deferred: ${params.isDeferred}');

      // Handle passthrough data
      if (params.passthrough != null) {
        handlePassthroughData(params.passthrough!);
      }

      // Handle deep link navigation
      if (params.deeplink != null) {
        handleDeepLinkNavigation(params.deeplink!, params.isDeferred);
      }
    };

    Singular.start(config);
  }

  void handlePassthroughData(String passthroughString) {
    try {
      final data = jsonDecode(passthroughString);

      // Apply promo code if present
      if (data.containsKey('promo_code')) {
        applyPromoCode(data['promo_code']);
      }

      // Set user segment
      if (data.containsKey('segment')) {
        setUserSegment(data['segment']);
      }

      // Track campaign
      if (data.containsKey('campaign_id')) {
        trackCampaign(data['campaign_id']);
      }
    } catch (error) {
      print('Error parsing passthrough: $error');
    }
  }

  void handleDeepLinkNavigation(String url, bool isDeferred) {
    // Parse URL to extract route and parameters
    final urlObj = parseDeepLink(url);

    print('Navigating to: ${urlObj['route']}');
    print('Parameters: ${urlObj['params']}');
    print('Deferred install: $isDeferred');

    // Route based on deep link structure
    switch (urlObj['route']) {
      case 'product':
        navigateToProduct(urlObj['params']['id']);
        break;
      case 'promo':
        navigateToPromo(urlObj['params']['code']);
        break;
      case 'category':
        navigateToCategory(urlObj['params']['name']);
        break;
      default:
        navigateToHome();
    }
  }

  Map<String, dynamic> parseDeepLink(String url) {
    // Parse myapp://product/123?variant=blue
    final parts = url.split('?');
    final path = parts[0];
    final queryString = parts.length > 1 ? parts[1] : null;

    // Extract path components
    final pathParts = path.replaceFirst(RegExp(r'^[^:]+://'), '').split('/');
    final route = pathParts[0];

    // Parse parameters
    final params = <String, String>{};

    // Add path parameters
    if (pathParts.length > 1) {
      params['id'] = pathParts[1];
    }

    // Add query parameters
    if (queryString != null) {
      queryString.split('&').forEach((pair) {
        final keyValue = pair.split('=');
        if (keyValue.length == 2) {
          params[keyValue[0]] = Uri.decodeComponent(keyValue[1]);
        }
      });
    }

    return {
      'route': route,
      'params': params
    };
  }

  // Navigation functions
  void navigateToProduct(String? productId) {
    if (productId != null) {
      print('Navigating to product: $productId');
      // Use your navigation framework (Navigator, GoRouter, etc.)
      navigatorKey.currentState?.pushNamed('/product/$productId');
    }
  }

  void navigateToPromo(String? promoCode) {
    if (promoCode != null) {
      print('Navigating to promo: $promoCode');
      navigatorKey.currentState?.pushNamed('/promo/$promoCode');
    }
  }

  void navigateToCategory(String? categoryName) {
    if (categoryName != null) {
      print('Navigating to category: $categoryName');
      navigatorKey.currentState?.pushNamed('/category/$categoryName');
    }
  }

  void navigateToHome() {
    print('Navigating to home');
    navigatorKey.currentState?.pushNamed('/');
  }

  // Utility functions
  void applyPromoCode(String code) {
    print('Applying promo code: $code');
    // Your promo code logic
  }

  void setUserSegment(String segment) {
    print('Setting user segment: $segment');
    // Your user segmentation logic
  }

  void trackCampaign(String campaignId) {
    print('Tracking campaign: $campaignId');
    // Your campaign tracking logic
  }

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

// Placeholder screen classes
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')),
    );
  }
}

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

ベストプラクティス:

  • URLを安全に解析:セキュリティ上の脆弱性を防ぐため、 ナビゲーション前にディープリンクURLを常に検証・サニタイズする
  • ナビゲーション状態の処理:MaterialApp が完全に初期化する前にナビゲーションを有効化するため、NavigatorState には GlobalKey を使用
  • 両シナリオのテスト:開発中に即時ディープリンク (アプリインストール済み) と遅延ディープリンク (新規インストール) の両方をテスト
  • デバッグ用ログ記録:開発中は包括的なログ記録を有効化し、 ディープリンクの解決とナビゲーションフローを追跡する
  • エラー処理:不正なデータを適切に処理するため、 JSON解析とナビゲーション操作にtry-catchブロックを実装