添加深度链接支持
深度链接可引导用户跳转至应用内的特定内容。当用户在已安装应用的设备上点击深度链接时,应用将直接打开目标内容(如产品页面或特定体验)。
Singular追踪链接同时支持标准深度链接(已安装应用)和延迟深度链接(新安装应用)。完整信息请参阅深度链接常见问题 解答和Singular链接常见问题解答。
要求
先决条件
完成Singular 链接先决条件以启用应用的深度链接功能。
注:
- 本文假设您的组织正在使用Singular Links——Singular的跟踪链接技术。早期客户可能仍在使用旧版跟踪链接。
- 应用的深度链接目标需在Singular的"应用"页面完成配置(详见《配置应用归因追踪》)。
可用参数
当应用启动时,SingularLink处理程序可访问来自Singular跟踪链接的深度链接、延迟深度链接及传递参数。
- 深度链接(_dl):用户点击链接后跳转的应用内目标网址
- 延迟深层链接(_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 端需进行两项变更:禁用 Flutter 内置深层链接处理程序,并手动将 intent 转发至 Flutter 引擎和 Singular SDK。
步骤1:禁用Flutter深层链接处理器
在AndroidManifest.xml 文件中,于<activity> 标签内添加以下<meta-data> 条目:
<meta-data
android:name="flutter_deeplinking_enabled"
android:value="false" />
步骤二:手动转发意图
更新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
通过在AppDelegate 文件中向Singular SDK传递launchOptions 和userActivity 对象,
启用Singular SDK处理启动相关数据并管理深度链接。
这些对象包含应用启动方式及原因的关键信息, 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,
从而启用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链接常见问题解答。
完整文档请参阅 singularLinksHandler 参考文档。
处理器行为
链接解析机制解析
singularLinksHandler 的行为取决于应用是新安装还是已安装状态:
全新安装(延迟深层链接)
首次安装时,应用启动时不存在 Open URL。Singular 完成归因以确定跟踪链接是否包含深层链接或延迟深层链接值。
延迟深层链接流程:
- 用户点击配置了深层链接值的Singular追踪链接
- 用户首次安装并打开应用
- Singular SDK将首次会话数据发送至服务器
- 归因完成并从跟踪链接中识别出深度链接
-
deeplink深度链接值通过isDeferred = true参数返回至singularLinksHandler回调函数,格式为
测试延迟深层链接:
- 从测试设备卸载应用(若当前已安装)
- iOS:重置IDFA。Android:重置Google广告ID(GAID)
- 在设备上点击Singular追踪链接(确保已配置深层链接值)
- 安装并打开应用
归因应成功完成,延迟深层链接值将传递至singularLinksHandler 处理程序。
专业技巧:使用不同包名(例如com.example.dev 而非com.example.prod )的开发版本测试深度链接时,请针对开发应用的包名专门配置跟踪链接。点击测试链接后,请直接将开发版本安装至设备(通过Android Studio或Xcode),而非从应用商店下载正式版应用。
已安装状态(即时深度链接)
当应用已安装时,点击Singular链接将通过通用链接(iOS)或Android应用链接技术立即打开应用。
即时深度链接流程:
- 用户点击Singular追踪链接
- 操作系统提供包含完整Singular跟踪链接的Open URL
- SDK初始化过程中解析该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');
}
转发所有查询参数
在跟踪链接后添加_forward_params=2 参数,即可捕获跟踪链接URL中的所有查询参数。
当在跟踪链接后添加_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, 防止安全漏洞
- 处理导航状态:使用 NavigatorState 的 GlobalKey 在 MaterialApp 完全初始化前启用导航
- 双场景测试:开发期间同时测试即时深层链接 (已安装应用)与延迟深层链接(全新安装)
- 调试日志记录:开发期间启用全面日志记录,追踪深层链接解析与导航流程
- 错误处理:为JSON解析和导航操作实现try-catch代码块, 优雅处理格式错误的数据