SDK do Flutter - Suporte a links diretos

Documento

Adicionando suporte a links diretos

Os links diretos direcionam os usuários para um conteúdo específico dentro do seu aplicativo. Quando os usuários tocam em um link direto em um dispositivo com o seu aplicativo instalado, o aplicativo abre diretamente no conteúdo pretendido, como uma página de produto ou uma experiência específica.

Os links de rastreamento da Singular oferecem suporte tanto a links diretos padrão (para aplicativos instalados) quanto a links diretos diferidos (para novas instalações). Para obter informações completas, consulte as Perguntas frequentes sobre links diretos e as Perguntas frequentes sobre links da Singular.


Requisitos

Pré-requisitos

Conclua os pré-requisitos dos links Singular para habilitar o deep linking para seu aplicativo.

Observações:

Parâmetros disponíveis

O manipulador SingularLink fornece acesso a parâmetros de links diretos, links diretos diferidos e passagem de parâmetros dos links de rastreamento da Singular quando o aplicativo é aberto.

  • Deep Link (_dl): o URL de destino dentro do seu aplicativo para usuários que clicam no link
  • Deep link diferido (_ddl): o URL de destino para usuários que instalam o aplicativo após clicar no link
  • Passthrough (_p): dados personalizados passados pelo link de rastreamento para contexto adicional

O Flutter 3.35 introduziu uma alteração significativa na forma como os links profundos são processados. A função interna didPushRouteInformation() agora intercepta e decodifica links profundos antes que SDKs de terceiros possam processá-los. Isso cria um conflito com o manipulador de links profundos do Singular, fazendo com que os links do Singular sejam tratados como rotas desconhecidas e direcionando os usuários para a tela inicial em vez de para o destino pretendido.

Isso afeta tanto o Android quanto o iOS. Se o seu aplicativo tem como alvo o Flutter 3.35 ou posterior, conclua a configuração adicional específica da plataforma abaixo antes de prosseguir com a implementação padrão de links diretos.

Observação: esta é uma alteração no nível da estrutura do Flutter, não um bug do SDK da Singular. Não é necessária nenhuma atualização do SDK. As configurações abaixo restauram o comportamento correto do roteamento de links profundos para todos os estados do aplicativo (primeiro plano, segundo plano e inicialização a frio).

Duas alterações são necessárias no Android: desativar o manipulador de links profundos integrado do Flutter e encaminhar manualmente as intenções para o mecanismo Flutter e o SDK Singular.

Em seu AndroidManifest.xml, adicione a seguinte entrada <meta-data> dentro da tag <activity>:

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

Atualize seu MainActivity para encaminhar intenções tanto para o mecanismo Flutter quanto para o SDK 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);
    }
}

Observação: isso substitui a implementação padrão do onNewIntent mostrada na seção Configuração da plataforma Android abaixo para projetos Flutter 3.35+. Se você estiver usando o Flutter 3.10.x ou anterior, use a implementação padrão.

No Info.plist do seu aplicativo, defina o sinalizador FlutterDeepLinkingEnabled como false para desativar a interceptação de links profundos nativos do Flutter:

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

Observação: não são necessárias alterações adicionais no AppDelegate além desse sinalizador. Continue com a configuração padrão do AppDelegate do iOS mostrada na seção Configuração da plataforma iOS abaixo.


Configuração da plataforma

Configuração da plataforma iOS

Atualize o iOS AppDelegate

Habilite o SDK Singular para processar dados relacionados ao lançamento e lidar com links profundos passando os objetos launchOptions e userActivity para o SDK Singular em seu arquivo AppDelegate.

Esses objetos contêm informações críticas sobre como e por que seu aplicativo foi iniciado, que o Singular usa para rastreamento de atribuição e navegação de links diretos .

Implementação em 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];
}

Implementação 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)
}

Configuração da plataforma Android

Atualize o Android MainActivity

Habilite o SDK Singular para processar dados relacionados ao lançamento e lidar com links profundos modificando o arquivo MainActivity para passar o objeto Intent para o SDK Singular.

O objeto Intent contém informações sobre como e por que seu aplicativo foi iniciado, que o Singular usa para rastreamento de atribuição e navegação de links profundos.

Implementação 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);
    }
}

Implementação 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)
    }
}

Configuração do SDK

Implementar o manipulador de links Singular

Configure o retorno de chamada singularLinksHandler durante a inicialização do SDK para lidar com dados de links diretos recebidos e links diretos 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(),
    );
  }
}

Propriedade de configuração:

void Function(SingularLinkParams)? singularLinksHandler

Observação: a chamada de retorno singularLinksHandler é acionada apenas quando o aplicativo é aberto por meio de um Singular Link. Para obter mais informações, consulte as Perguntas frequentes sobre Singular Links.

Para obter a documentação completa, consulte a referência singularLinksHandler.


Comportamento do manipulador

Entendendo a resolução de links

O singularLinksHandler se comporta de maneira diferente dependendo se o aplicativo foi instalado recentemente ou já estava instalado.

Instalação nova (deep link diferido)

Em uma instalação nova, não existe um URL aberto quando o aplicativo é iniciado. O Singular conclui a atribuição para determinar se o link de rastreamento continha um valor de link direto ou link direto diferido.

Fluxo de link direto diferido:

  1. O usuário clica em um link de rastreamento Singular configurado com um valor de deep link
  2. O usuário instala e abre o aplicativo pela primeira vez
  3. O SDK do Singular envia a primeira sessão para os servidores do Singular
  4. A atribuição é concluída e identifica o deep link a partir do link de rastreamento
  5. O valor do deep link retorna para a chamada de retorno singularLinksHandler no parâmetro deeplink com isDeferred = true

Testando links profundos diferidos:

  1. Desinstale o aplicativo do dispositivo de teste (se estiver instalado)
  2. iOS: redefina seu IDFA. Android: redefina seu Google Advertising ID (GAID)
  3. Clique no link de rastreamento Singular no dispositivo (certifique-se de que ele esteja configurado com um valor de link direto)
  4. Instale e abra o aplicativo

A atribuição deve ser concluída com sucesso, e o valor do link direto diferido será passado para o manipulador singularLinksHandler.

Dica profissional: ao testar links diretos com uma versão de desenvolvimento usando um nome de pacote diferente (por exemplo, com.example.dev em vez de com.example.prod), configure o link de rastreamento especificamente para o nome do pacote do aplicativo de desenvolvimento. Depois de clicar no link de teste, instale a versão de desenvolvimento diretamente no dispositivo (via Android Studio ou Xcode) em vez de baixar o aplicativo de produção da loja de aplicativos.


Já instalado (link direto imediato)

Quando o aplicativo já está instalado, clicar em um Singular Link abre o aplicativo imediatamente usando a tecnologia Universal Links (iOS) ou Android App Links.

Fluxo de deep link imediato:

  1. O usuário clica em um link de rastreamento Singular
  2. O sistema operacional fornece um Open URL contendo o link de rastreamento Singular completo
  3. Durante a inicialização do SDK, o Singular analisa a URL
  4. O Singular extrai os valores deeplink e passthrough
  5. Os valores são retornados através do manipulador singularLinksHandler com isDeferred = false

Recursos avançados

Parâmetros de passagem

Capture dados adicionais do clique no link de rastreamento usando parâmetros de passagem .

Se um parâmetro passthrough (_p) estiver incluído no link de rastreamento , o parâmetro passthrough do manipulador singularLinksHandler contém os dados correspondentes. Use isso para capturar metadados da campanha, dados de segmentação do usuário ou qualquer informação personalizada necessária no aplicativo.

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

Encaminhar todos os parâmetros de consulta

Capture todos os parâmetros de consulta da URL do link de rastreamento anexando o parâmetro _forward_params=2 ao seu link de rastreamento.

Quando _forward_params=2 é adicionado ao link de rastreamento, todos os parâmetros de consulta são incluídos no parâmetro deeplink do manipulador singularLinksHandler, dando a você acesso ao URL completo com todos os seus parâmetros.

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

O manipulador singularLinksHandler receberá:
deeplink = "myapp://product/123?utm_source=facebook&promo=SALE2024"


Exemplo de implementação completa

Implementação abrangente de deep linking com navegação, tratamento de passagem e encaminhamento de parâmetros para aplicativos 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áticas recomendadas:

  • Analise URLs com segurança: sempre valide e sanitize URLs de links diretos antes da navegação para evitar vulnerabilidades de segurança
  • Lidar com o estado de navegação: use GlobalKey para NavigatorState para habilitar a navegação antes que o MaterialApp seja totalmente inicializado
  • Teste os dois cenários: teste links diretos imediatos (aplicativo instalado) e links diretos diferidos (nova instalação) durante o desenvolvimento
  • Registre para depuração: habilite o registro abrangente durante o desenvolvimento para rastrear a resolução de links profundos e o fluxo de navegação .
  • Tratamento de erros: implemente blocos try-catch para análise JSON e operações de navegação para lidar com dados malformados de maneira elegante