SDK do Flutter - Suporte a Deep Links

Documento

Adição de suporte para Deep Linking

Os links direcionam os usuários para um conteúdo específico dentro do seu aplicativo. Quando os utilizadores tocam numa ligação direta num dispositivo com a sua aplicação instalada, a aplicação abre-se diretamente para o conteúdo pretendido, como uma página de produto ou uma experiência específica.

Os links de rastreamento singulares suportam links diretos padrão (para aplicativos instalados) e links diretos adiados (para novas instalações). Para obter informações abrangentes, consulte as Perguntas frequentes sobre links diretos e as Perguntas frequentes sobre links singulares.


Requisitos

Pré-requisitos

Preencha os Pré-requisitos do Singular Links para habilitar o deep linking para seu aplicativo.

Observações:

  • Este artigo pressupõe que sua organização está usando Singular Links - a tecnologia de link de rastreamento da Singular. Os clientes mais antigos podem estar usando links de rastreamento herdados.
  • Os destinos de deep link do seu aplicativo precisam ser configurados na página Aplicativos no Singular (consulte Configurando seu aplicativo para rastreamento de atribuição).

Parâmetros disponíveis

O manipulador SingularLink fornece acesso a parâmetros de deep link, deferred deep link e passthrough de links de rastreamento Singular quando o aplicativo é aberto.

  • Deep Link (_dl): O URL de destino dentro do seu aplicativo para usuários que clicam no link
  • Deferred Deep Link (_ddl): O URL de destino para os utilizadores que instalam a aplicação depois de clicarem no link
  • Passthrough (_p): Dados personalizados passados através do link de rastreamento para contexto adicional

Configuração da plataforma

Configuração da plataforma iOS

Atualizar iOS AppDelegate

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

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

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 em 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

Atualizar a MainActivity do Android

Habilite o SDK Singular para processar dados relacionados à inicialização e manipular 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 lançado, que o Singular usa para rastreamento de atribuição e navegação de link profundo.

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 do 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 manipular dados de deep link recebidos e dados de deep link adiados.

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 somente quando o aplicativo é aberto por meio de um link singular. Para obter mais informações, consulte as Perguntas frequentes sobre links singulares.

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 se já está instalado.

Instalação recente (Deep Link diferido)

Em uma instalação recente, não existe uma URL aberta quando o aplicativo é iniciado. A Singular completa a atribuição para determinar se o link de rastreamento contém um valor de deep link ou de deferred deep link.

Fluxo de Deep Link Diferido:

  1. O usuário clica em um link de rastreamento da Singular configurado com um valor de deep link
  2. O usuário instala e abre o aplicativo pela primeira vez
  3. O Singular SDK envia a primeira sessão para os servidores 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 o retorno de chamada singularLinksHandler no parâmetro deeplink com isDeferred = true

Testando Deep Links adiados:

  1. Desinstale o aplicativo do dispositivo de teste (se estiver instalado no momento)
  2. iOS: Redefinir seu IDFA. Android: Redefinir seu Google Advertising ID (GAID)
  3. Clique no link de rastreamento Singular do dispositivo (verifique se ele está configurado com um valor de link profundo)
  4. Instale e abra o aplicativo

A atribuição deve ser concluída com êxito e o valor diferido do deep link será passado para o manipulador singularLinksHandler.

Dica profissional: Ao testar links diretos com uma compilaçã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 compilação de desenvolvimento diretamente no dispositivo (através do Android Studio ou Xcode) em vez de transferir a aplicação de produção a partir da loja de aplicações.


Já instalado (ligação direta imediata)

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

Fluxo imediato do Deep Link:

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

Recursos avançados

Parâmetros de passagem

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

Se um parâmetro passthrough (_p) for incluído no link de rastreamento, o parâmetro passthrough do manipulador singularLinksHandler conterá os dados correspondentes. Utilize-o para capturar metadados de campanha, dados de segmentação de utilizadores ou qualquer informação personalizada de que necessite na aplicação.

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 do 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-lhe 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, manipulação 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')),
    );
  }
}

Melhores Práticas:

  • Analisar URLs com segurança: Sempre valide e higienize URLs de deep link 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 ambos os cenários: Teste tanto os deep links imediatos (aplicativo instalado) quanto os diferidos (nova instalação) durante o desenvolvimento
  • Registrar para depuração: Habilite o registro abrangente durante o desenvolvimento para rastrear a resolução do deep link e o fluxo de navegação
  • Tratamento de erros: Implemente blocos try-catch para análise de JSON e operações de navegação para tratar graciosamente dados malformados