ディープリンク機能の追加
ディープリンクは、アプリ内の特定のコンテンツへユーザーを誘導します。アプリがインストールされた端末でディープリンクをタップすると、商品ページや特定の体験など、意図したコンテンツに直接アプリが開きます。
Singularトラッキングリンクは、標準ディープリンク(インストール済みアプリ向け)と遅延ディープリンク(新規インストール向け)の両方をサポートします。詳細については、ディープリンクFAQおよび SingularリンクFAQを参照してください。
要件
前提条件
アプリのディープリンクを有効化するには、Singularリンクの前提条件を完了してください。
注記:
- 本記事は、組織がSingular Links(Singularのトラッキングリンク技術)を利用していることを前提としています。従来型のトラッキングリンクを使用しているお客様もいらっしゃる可能性があります。
- アプリのディープリンク先は、Singularの「Apps」ページで設定する必要があります(アプリのアトリビューション追跡設定を参照)。
利用可能なパラメータ
SingularLinkハンドラーは、アプリ起動時にSingularトラッキングリンクからディープリンク、遅延ディープリンク、パススルーパラメータへのアクセスを提供します。
- ディープリンク(_dl):リンクをクリックしたユーザー向けのアプリ内到達先URL
- 遅延ディープリンク(_ddl):リンククリック後にアプリをインストールしたユーザー向けのリダイレクト先URL
- パススルー (_p):追加コンテキスト用のトラッキングリンク経由で渡されるカスタムデータ
Flutter バージョン互換性
Flutter 3.35 の互換性破綻変更
Flutter 3.35では、ディープリンクの処理方法に互換性のない変更が導入されました。
内部のdidPushRouteInformation() 関数が、サードパーティSDKが処理する前に
ディープリンクをインターセプトしてデコードするようになりました。これにより
Singularのディープリンクハンドラーとの競合が発生し、Singularリンクが
未知のルートとして扱われ、意図した目的地ではなくホーム画面に
ユーザーが誘導される問題が生じます。
これはAndroidとiOSの両方に影響します。アプリが Flutter 3.35以降を対象としている場合、標準的な ディープリンク実装を進める前に、以下のプラットフォーム固有の 追加設定を完了してください。
注:これは Flutter フレームワークレベルの変更であり、 Singular SDK のバグではありません。SDK の更新は不要です。以下の設定により、 すべてのアプリ状態(フォアグラウンド、バックグラウンド、コールドスタート)で 正しいディープリンクルーティング動作が復元されます。
Android設定(Flutter 3.35以降)
Androidでは2つの変更が必要です:Flutterの組み込みディープリンクハンドラーを無効化し、 インテントをFlutterエンジンとSingular SDKの両方に手動で転送すること。
ステップ1: Flutterのディープリンクハンドラーを無効化
AndroidManifest.xml ファイルの<activity> タグ内に、以下の<meta-data> エントリを追加します:
<meta-data
android:name="flutter_deeplinking_enabled"
android:value="false" />
ステップ2: インテントの手動転送
MainActivity を更新し、FlutterエンジンとSingular SDKの両方に
インテントを転送するようにします。
// 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 以前の場合は、
代わりに標準実装を使用してください。
iOS設定(Flutter 3.35+)
アプリのInfo.plist で、FlutterDeepLinkingEnabled フラグをfalse に設定し、
Flutterのネイティブディープリンクインターセプトを無効化します:
<key>FlutterDeepLinkingEnabled</key>
<false/>
注:このフラグ以外に
AppDelegate への追加変更は不要です。
下記の「iOSプラットフォーム設定」セクションで示す
標準的なiOS AppDelegate設定を続けてください。
プラットフォーム設定
iOSプラットフォーム設定
iOS AppDelegateの更新
Singular SDKが起動関連データを処理しディープリンクを管理できるよう、AppDelegate ファイル内で
Singular SDKにlaunchOptions およびuserActivity オブジェクトを渡します。
これらのオブジェクトには、アプリの起動方法や起動理由に関する重要な情報が含まれており、 Singularはこれをアトリビューション追跡やディープリンクナビゲーションに利用します。
Objective-C実装
// 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実装
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実装
// 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実装
// 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 コールバックを設定し、
受信するディープリンクおよび遅延ディープリンクデータを処理します。
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 はアトリビューションを完了し、トラッキングリンクにディープリンクまたは遅延ディープリンクの値が含まれていたかを判断します。
遅延ディープリンクのフロー:
- ユーザーがディープリンク値を設定したSingularトラッキングリンクをクリック
- ユーザーがアプリをインストールし初回起動
- Singular SDKが最初のセッションをSingularサーバーに送信
- アトリビューションが完了し、トラッキングリンクからディープリンクを特定
-
deeplinkディープリンク値がisDeferred = trueパラメータでsingularLinksHandlerコールバックに返される( を含む)
遅延ディープリンクのテスト:
- テスト端末からアプリをアンインストール(現在インストールされている場合)
- iOS:IDFAをリセットしてください。Android:Google広告ID (GAID) をリセットしてください
- デバイスからSingularトラッキングリンクをクリック(ディープリンク値が設定されていることを確認)
- アプリをインストールして起動する
アトリビューションが正常に完了し、遅延ディープリンクの値がsingularLinksHandler ハンドラーに渡されます。
プロのヒント:異なるパッケージ名(例:com.example.prod ではなくcom.example.dev )を使用した開発ビルドでディープリンクをテストする場合、開発アプリのパッケージ名に合わせてトラッキングリンクを特別に設定してください。テストリンクをクリックした後、アプリストアから本番アプリをダウンロードするのではなく、Android StudioまたはXcode経由で開発ビルドを直接デバイスにインストールしてください。
既にインストール済み(即時ディープリンク)
アプリが既にインストールされている場合、Singularリンクをクリックすると、ユニバーサルリンク(iOS)またはAndroidアプリリンク技術を使用してアプリが即座に起動します。
即時ディープリンクのフロー:
- ユーザーがSingularトラッキングリンクをクリック
- OSがSingularトラッキングリンク全体を含むOpen URLを提供
- SDK初期化時にSingularがURLを解析
- Singularが
deeplinkとpassthroughの値を抽出 - 値は
singularLinksHandlerハンドラー経由でisDeferred = falseと共に返される
高度な機能
パススルーパラメータ
パススルーパラメータを使用して、トラッキングリンクのクリックから追加データを取得します。 パラメータ。
トラッキングリンクにpassthrough (_p) パラメータが含まれている場合、singularLinksHandler ハンドラのpassthrough パラメータに対応するデータが含まれます。
キャンペーンメタデータ、ユーザーセグメンテーションデータ、
アプリ内で必要なカスタム情報の取得に利用します。
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アプリ向けのナビゲーション、パススルー処理、パラメータ転送を包括的に実装したディープリンク。
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ブロックを実装