SDK de Flutter: compatibilidad con enlaces profundos

Documento

Añadir compatibilidad con enlaces profundos

Los enlaces profundos dirigen a los usuarios a contenido específico dentro de su aplicación. Cuando los usuarios pulsan un enlace profundo en un dispositivo que tiene su aplicación instalada, la aplicación se abre directamente al contenido deseado, como la página de un producto o una experiencia específica.

Los enlaces de seguimiento de Singular admiten tanto enlaces profundos estándar (para aplicaciones instaladas) como enlaces profundos diferidos (para nuevas instalaciones). Para obtener información completa, consulte las preguntas frecuentes sobre enlaces profundos y las preguntas frecuentes sobre enlaces de Singular.


Requisitos

Requisitos

Complete los requisitos previos de enlaces Singular para habilitar los enlaces profundos para su aplicación.

Notas

Parámetros disponibles

El controlador SingularLink proporciona acceso a los parámetros de enlaces profundos, enlaces profundos diferidos y paso a través de los enlaces de seguimiento de Singular cuando se abre la aplicación.

  • Enlace profundo (_dl): la URL de destino dentro de su aplicación para los usuarios que hacen clic en el enlace.
  • Enlace profundo diferido (_ddl): la URL de destino para los usuarios que instalan la aplicación después de hacer clic en el enlace.
  • Passthrough (_p): datos personalizados que se transmiten a través del enlace de seguimiento para obtener contexto adicional.

Flutter 3.35 introdujo un cambio importante en la forma en que se procesan los enlaces profundos. La función interna didPushRouteInformation() ahora intercepta y decodifica los enlaces profundos antes de que los SDK de terceros puedan gestionarlos. Esto crea un conflicto con el gestor de enlaces profundos de Singular, lo que hace que los enlaces de Singular se traten como rutas desconocidas y dirijan a los usuarios a la pantalla de inicio en lugar de al destino previsto.

Esto afecta tanto a Android como a iOS. Si su aplicación está dirigida a Flutter 3.35 o posterior, complete la configuración adicional específica de la plataforma que se indica a continuación antes de continuar con la implementación estándar de enlaces profundos.

Nota: Se trata de un cambio a nivel del marco de Flutter, no de un error del SDK de Singular. No es necesario actualizar el SDK. Las configuraciones siguientes restablecen el comportamiento correcto del enrutamiento de enlaces profundos para todos los estados de la aplicación (primer plano, segundo plano y arranque en frío).

Se requieren dos cambios en Android: desactivar el controlador de enlaces profundos integrado en Flutter y reenviar manualmente las intenciones tanto al motor Flutter como al SDK de Singular.

En su AndroidManifest.xml, añada la siguiente entrada <meta-data> dentro de la etiqueta <activity>:

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

Actualice su MainActivity para reenviar las intenciones tanto al motor Flutter como al SDK de Singular.

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);
    }
}

Nota: Esto sustituye a la implementación estándar de onNewIntent que se muestra en la sección Configuración de la plataforma Android más abajo para proyectos Flutter 3.35+. Si utiliza Flutter 3.10.x o anterior, utilice la implementación estándar en su lugar.

En el Info.plist de su aplicación, establezca el indicador FlutterDeepLinkingEnabled en false para desactivar la interceptación de enlaces profundos nativos de Flutter:

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

Nota: No es necesario realizar cambios adicionales en AppDelegate más allá de esta bandera. Continúe con la configuración estándar de AppDelegate para iOS que se muestra en la sección Configuración de la plataforma iOS más abajo.


Configuración de la plataforma

Configuración de la plataforma iOS

Actualizar iOS AppDelegate

Habilite el SDK de Singular para procesar los datos relacionados con el lanzamiento y gestionar los enlaces profundos pasando los objetos launchOptions y userActivity al SDK de Singular en su archivo AppDelegate.

Estos objetos contienen información crítica sobre cómo y por qué se inició su aplicación, que Singular utiliza para el seguimiento de atribuciones y la navegación por enlaces profundos .

Implementación en 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];
}

Implementación en 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)
}

Configuración de la plataforma Android

Actualizar Android MainActivity

Habilita el SDK de Singular para procesar datos relacionados con el lanzamiento y gestionar enlaces profundos modificando el archivo MainActivity para pasar el objeto Intent al SDK de Singular.

El objeto Intent contiene información sobre cómo y por qué se inició su aplicación, que Singular utiliza para el seguimiento de atribuciones y la navegación por enlaces profundos.

Implementación en 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);
    }
}

Implementación en 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)
    }
}

Configuración del SDK

Implementar el controlador de enlaces Singular

Configure la devolución de llamada singularLinksHandler durante la inicialización del SDK para gestionar los datos de enlaces profundos entrantes y diferidos.

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(),
    );
  }
}

Propiedad de configuración:

void Function(SingularLinkParams)? singularLinksHandler

Nota: La devolución de llamada singularLinksHandler solo se activa cuando la aplicación se abre a través de un enlace Singular. Para obtener más información, consulte las preguntas frecuentes sobre enlaces Singular.

Para obtener la documentación completa, consulte la referencia singularLinksHandler.


Comportamiento del controlador

Comprensión de la resolución de enlaces

singularLinksHandler se comporta de forma diferente dependiendo de si la aplicación está recién instalada o ya instalada.

Instalación nueva (enlace profundo diferido)

En una instalación nueva, no existe ninguna URL abierta cuando se inicia la aplicación. Singular completa la atribución para determinar si el enlace de seguimiento contenía un enlace profundo o un valor de enlace profundo diferido.

Flujo de enlaces profundos diferidos:

  1. El usuario hace clic en un enlace de seguimiento de Singular configurado con un valor de enlace profundo.
  2. El usuario instala y abre la aplicación por primera vez
  3. El SDK de Singular envía la primera sesión a los servidores de Singular
  4. La atribución se completa e identifica el enlace profundo a partir del enlace de seguimiento
  5. El valor del enlace profundo vuelve a la llamada de retorno singularLinksHandler en el parámetro deeplink con isDeferred = true

Prueba de enlaces profundos diferidos:

  1. Desinstala la aplicación del dispositivo de prueba (si está instalada actualmente).
  2. iOS: restablece tu IDFA. Android: restablece tu ID de publicidad de Google (GAID)
  3. Haga clic en el enlace de seguimiento de Singular desde el dispositivo (asegúrese de que esté configurado con un valor de enlace profundo).
  4. Instale y abra la aplicación.

La atribución debería completarse correctamente y el valor del enlace profundo diferido se pasará al controlador singularLinksHandler.

Consejo profesional: cuando pruebes enlaces profundos con una versión de desarrollo que utilice un nombre de paquete diferente (por ejemplo, com.example.dev en lugar de com.example.prod), configura el enlace de seguimiento específicamente para el nombre del paquete de la aplicación de desarrollo. Después de hacer clic en el enlace de prueba, instala la versión de desarrollo directamente en el dispositivo (a través de Android Studio o Xcode) en lugar de descargar la aplicación de producción desde la tienda de aplicaciones.


Ya instalado (enlace profundo inmediato)

Cuando la aplicación ya está instalada, al hacer clic en un enlace Singular, la aplicación se abre inmediatamente utilizando la tecnología Universal Links (iOS) o Android App Links.

Flujo de enlace profundo inmediato:

  1. El usuario hace clic en un enlace de seguimiento de Singular
  2. El sistema operativo proporciona una URL abierta que contiene el enlace de seguimiento completo de Singular
  3. Durante la inicialización del SDK, Singular analiza la URL
  4. Singular extrae los valores deeplink y passthrough
  5. Los valores se devuelven a través del controlador singularLinksHandler con isDeferred = false

Funciones avanzadas

Parámetros de paso

Captura datos adicionales del clic en el enlace de seguimiento utilizando parámetros de paso. .

Si se incluye un parámetro passthrough (_p) en el enlace de seguimiento, el parámetro passthrough del controlador singularLinksHandler contiene los datos correspondientes. Utilícelo para capturar metadatos de la campaña, datos de segmentación de usuarios o cualquier información personalizada que necesite en la aplicación.

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');
}

Reenviar todos los parámetros de consulta

Capture todos los parámetros de consulta de la URL del enlace de seguimiento añadiendo el parámetro _forward_params=2 a su enlace de seguimiento.

Cuando se añade _forward_params=2 al enlace de seguimiento, todos los parámetros de consulta se incluyen en el parámetro deeplink del controlador singularLinksHandler, lo que le da acceso a la URL completa con todos sus parámetros.

Ejemplo de enlace de seguimiento:
https://yourapp.sng.link/A1b2c/abc123?_dl=myapp://product/123&_forward_params=2&utm_source=facebook&promo=SALE2024

El controlador singularLinksHandler recibirá:
deeplink = "myapp://product/123?utm_source=facebook&promo=SALE2024"


Ejemplo de implementación completa

Implementación completa de enlaces profundos con navegación, gestión de passthrough y reenvío de parámetros para aplicaciones 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')),
    );
  }
}

Prácticas recomendadas:

  • Analizar las URL de forma segura: valide y desinfecte siempre las URL de enlaces profundos antes de la navegación para evitar vulnerabilidades de seguridad
  • Gestionar el estado de navegación: utilice GlobalKey para NavigatorState para habilitar la navegación antes de que MaterialApp se inicialice por completo
  • Pruebe ambos escenarios: pruebe tanto los enlaces profundos inmediatos (aplicación instalada) como los enlaces profundos diferidos (instalación nueva) durante el desarrollo
  • Registro para la depuración: habilite el registro completo durante el desarrollo para rastrear la resolución de enlaces profundos y el flujo de navegación .
  • Gestión de errores: implemente bloques try-catch para el análisis JSON y las operaciones de navegación para gestionar correctamente los datos malformados .