Flutter SDK - 支持深度链接

文档

添加深度链接支持

深度链接可引导用户跳转至应用内的特定内容。当用户在已安装应用的设备上点击深度链接时,应用将直接打开目标内容(如产品页面或特定体验)。

Singular追踪链接同时支持标准深度链接(已安装应用)和延迟深度链接(新安装应用)。完整信息请参阅深度链接常见问题 解答和Singular链接常见问题解答


要求

先决条件

完成Singular 链接先决条件以启用应用的深度链接功能。

注:

  • 本文假设您的组织正在使用Singular Links——Singular的跟踪链接技术。早期客户可能仍在使用旧版跟踪链接。
  • 应用的深度链接目标需在Singular的"应用"页面完成配置(详见《配置应用归因追踪》)。

可用参数

当应用启动时,SingularLink处理程序可访问来自Singular跟踪链接的深度链接、延迟深度链接及传递参数。

  • 深度链接(_dl):用户点击链接后跳转的应用内目标网址
  • 延迟深层链接(_ddl):用户点击链接后安装应用时的目标URL
  • 传递参数(_p):通过追踪链接传递的自定义数据,用于补充上下文信息

Flutter 3.35 对深度链接处理方式进行了破坏性变更。 内部函数 `didPushRouteInformation() ` 现会在第三方 SDK 处理前 拦截并解码深度链接。这导致与 Singular 的深度链接处理程序冲突, 使 Singular 链接被视为未知路由,将用户导向主屏幕而非 预期目标页面。

此问题同时影响Android和iOS平台。若您的应用基于 Flutter 3.35及以上版本,请在执行标准深层链接实现前 完成以下平台专属配置:

注意:此为 Flutter 框架层面的变更, 并非 Singular SDK 缺陷。无需更新 SDK。 以下配置可恢复所有应用状态(前台、后台及冷启动) 下的正确深度链接路由行为。

Android 端需进行两项变更:禁用 Flutter 内置深层链接处理程序,并手动将 intent 转发至 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

通过在AppDelegate 文件中向Singular SDK传递launchOptionsuserActivity 对象, 启用Singular SDK处理启动相关数据并管理深度链接。

这些对象包含应用启动方式及原因的关键信息, 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, 从而启用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链接常见问题解答

完整文档请参阅 singularLinksHandler 参考文档


处理器行为

链接解析机制解析

singularLinksHandler 的行为取决于应用是新安装还是已安装状态:

全新安装(延迟深层链接)

首次安装时,应用启动时不存在 Open URL。Singular 完成归因以确定跟踪链接是否包含深层链接或延迟深层链接值。

延迟深层链接流程:

  1. 用户点击配置了深层链接值的Singular追踪链接
  2. 用户首次安装并打开应用
  3. Singular SDK将首次会话数据发送至服务器
  4. 归因完成并从跟踪链接中识别出深度链接
  5. deeplink 深度链接值通过isDeferred = true参数返回至singularLinksHandler 回调函数,格式为

测试延迟深层链接:

  1. 从测试设备卸载应用(若当前已安装)
  2. iOS:重置IDFA。Android:重置Google广告ID(GAID)
  3. 在设备上点击Singular追踪链接(确保已配置深层链接值)
  4. 安装并打开应用

归因应成功完成,延迟深层链接值将传递至singularLinksHandler 处理程序。

专业技巧:使用不同包名(例如com.example.dev 而非com.example.prod )的开发版本测试深度链接时,请针对开发应用的包名专门配置跟踪链接。点击测试链接后,请直接将开发版本安装至设备(通过Android Studio或Xcode),而非从应用商店下载正式版应用。


已安装状态(即时深度链接)

当应用已安装时,点击Singular链接将通过通用链接(iOS)或Android应用链接技术立即打开应用。

即时深度链接流程:

  1. 用户点击Singular追踪链接
  2. 操作系统提供包含完整Singular跟踪链接的Open URL
  3. SDK初始化过程中解析该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');
}

转发所有查询参数

在跟踪链接后添加_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应用提供包含导航、参数传递处理及参数转发的全面深度链接实现方案。

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, 防止安全漏洞
  • 处理导航状态:使用 NavigatorState 的 GlobalKey 在 MaterialApp 完全初始化前启用导航
  • 双场景测试:开发期间同时测试即时深层链接 (已安装应用)与延迟深层链接(全新安装)
  • 调试日志记录:开发期间启用全面日志记录,追踪深层链接解析与导航流程
  • 错误处理:为JSON解析和导航操作实现try-catch代码块, 优雅处理格式错误的数据