Skip to main content

Receive Links in Flutter

Handle incoming deep links in your Flutter app using the Ulinkly SDK's reactive streams.

Prerequisites​

Before receiving links, ensure you've completed the SDK setup:

SDK Integration​

  • Flutter SDK plugin added to your pubspec.yaml and installed
  • ULink.instance initialized with your API key and configuration
  • iOS platform configured (bundle ID, URL scheme, associated domains if using universal links)
  • Android platform configured (package name, intent filters, permissions)

Ulinkly Dashboard Configuration​

  • Project settings configured in the Ulinkly dashboard for both platforms
  • API key generated from Dashboard → Settings → API Keys → Generate API Key
  • Domain configured in your project (Domains → Add Domain)

If you haven't completed these steps, follow the Flutter Getting Started guide to set up the SDK and configure your project.

The Ulinkly Flutter SDK exposes Dart streams that emit resolved link data when users tap your links. The SDK handles all the native platform integration automatically, so you just need to listen to the streams.

The SDK provides two separate streams for different link types:

Step 1.1: Set Up Stream Listeners​

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_ulink_sdk/flutter_ulink_sdk.dart';
import 'package:flutter_ulink_sdk/models/models.dart';

class MyApp extends StatefulWidget {
const MyApp({super.key});


State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
final _sdk = ULink.instance;
StreamSubscription<ULinkResolvedData>? _dynamicSub;
StreamSubscription<ULinkResolvedData>? _unifiedSub;


void initState() {
super.initState();
_setupLinkListeners();
}


void dispose() {
_dynamicSub?.cancel();
_unifiedSub?.cancel();
super.dispose();
}

void _setupLinkListeners() {
// Listen for dynamic links (in-app handling)
_dynamicSub = _sdk.onDynamicLink.listen((linkData) {
_handleDynamicLink(linkData);
});

// Listen for unified links (marketing/redirect links)
_unifiedSub = _sdk.onUnifiedLink.listen((linkData) {
_handleUnifiedLink(linkData);
});
}

void _handleDynamicLink(ULinkResolvedData data) {
debugPrint('Dynamic link received:');
debugPrint(' Slug: ${data.slug}');
debugPrint(' Parameters: ${data.parameters}');

// Navigate based on parameters
if (data.parameters != null) {
final screen = data.parameters!['screen'] as String?;
if (screen != null) {
_navigateToScreen(screen, data.parameters);
}
}
}

void _handleUnifiedLink(ULinkResolvedData data) {
debugPrint('Unified link received:');
debugPrint(' Slug: ${data.slug}');
// Unified links typically don't have parameters
// Handle marketing/redirect links here
}

void _navigateToScreen(String screen, Map<String, dynamic>? parameters) {
// Your navigation logic
// For example, using Navigator or a routing package
}


Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
home: HomeScreen(),
);
}
}
Stream Types
  • onDynamicLink: Emits dynamic links with parameters for in-app navigation
  • onUnifiedLink: Emits unified links for marketing/redirect scenarios
  • Both streams emit ULinkResolvedData objects containing the resolved link information
Stream Cleanup

Always cancel stream subscriptions in dispose() to prevent memory leaks. The SDK uses broadcast streams, so multiple listeners can subscribe to the same stream.

The SDK provides methods to access links that arrived before your app was ready or after a cold start.

When your app launches from a link, you can retrieve the initial link data:

Future<void> _checkInitialLink() async {
final initialLink = await _sdk.getInitialDeepLink();
if (initialLink != null) {
debugPrint('App launched from link: ${initialLink.slug}');
// Navigate to the appropriate screen
_handleDynamicLink(initialLink);
}
}

Retrieve the last resolved link data (useful for resuming state after app restarts):

Future<void> _checkLastLink() async {
final lastLink = await _sdk.getLastLinkData();
if (lastLink != null) {
debugPrint('Last link: ${lastLink.slug}');
// Use lastLink to restore app state
_handleDynamicLink(lastLink);
}
}
Persistence Required

getLastLinkData() only emits data if you initialized the SDK with persistLastLinkData: true in ULinkConfig (the Flutter default is false). Enable it when you need this behavior:

await ULink.instance.initialize(
ULinkConfig(
apiKey: 'your-api-key',
persistLastLinkData: true, // Enable persistence
),
);

3. Complete Example​

Here's a complete example showing how to set up link reception in a Flutter app:

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_ulink_sdk/flutter_ulink_sdk.dart';
import 'package:flutter_ulink_sdk/models/models.dart';

void main() async {
WidgetsFlutterBinding.ensureInitialized();

// Initialize Ulinkly SDK
await ULink.instance.initialize(
ULinkConfig(
apiKey: 'your-api-key',
debug: true,
persistLastLinkData: true,
),
);

runApp(const MyApp());
}

class MyApp extends StatefulWidget {
const MyApp({super.key});


State<MyApp> createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> {
final _sdk = ULink.instance;
StreamSubscription<ULinkResolvedData>? _dynamicSub;
StreamSubscription<ULinkResolvedData>? _unifiedSub;


void initState() {
super.initState();
_setupLinkListeners();
_checkInitialLink();
}


void dispose() {
_dynamicSub?.cancel();
_unifiedSub?.cancel();
super.dispose();
}

void _setupLinkListeners() {
debugPrint('Setting up link listeners');

// Listen for dynamic links
_dynamicSub = _sdk.onDynamicLink.listen((linkData) {
debugPrint('Dynamic link received: ${linkData.toJson()}');
_handleLink(linkData);
}, onError: (error) {
debugPrint('Error in dynamic link stream: $error');
});

// Listen for unified links
_unifiedSub = _sdk.onUnifiedLink.listen((linkData) {
debugPrint('Unified link received: ${linkData.toJson()}');
_handleUnifiedLink(linkData);
}, onError: (error) {
debugPrint('Error in unified link stream: $error');
});

debugPrint('Link listeners setup complete');
}

Future<void> _checkInitialLink() async {
try {
final initialLink = await _sdk.getInitialDeepLink();
if (initialLink != null) {
debugPrint('App launched from link: ${initialLink.slug}');
_handleLink(initialLink);
}
} catch (e) {
debugPrint('Error getting initial link: $e');
}
}

void _handleLink(ULinkResolvedData data) {
// Navigate based on link data
final parameters = data.parameters;
if (parameters != null) {
final screen = parameters['screen'] as String?;
switch (screen) {
case 'product':
final productId = parameters['productId'] as String?;
if (productId != null) {
_navigateToProduct(productId);
}
break;
case 'profile':
final userId = parameters['userId'] as String?;
if (userId != null) {
_navigateToProfile(userId);
}
break;
default:
debugPrint('Unknown screen: $screen');
}
}
}

void _handleUnifiedLink(ULinkResolvedData data) {
// Handle marketing/redirect links
debugPrint('Unified link received: ${data.slug}');
// Optionally open externally using url_launcher
// final url = data.iosUrl ?? data.androidUrl ?? data.fallbackUrl;
// if (url != null) {
// launchUrl(Uri.parse(url));
// }
}

void _navigateToProduct(String productId) {
// Your navigation logic
// For example, using Navigator or a routing package like go_router
Navigator.of(context).pushNamed('/product', arguments: productId);
}

void _navigateToProfile(String userId) {
// Your navigation logic
Navigator.of(context).pushNamed('/profile', arguments: userId);
}


Widget build(BuildContext context) {
return MaterialApp(
title: 'My App',
home: HomeScreen(),
routes: {
'/product': (context) => ProductScreen(),
'/profile': (context) => ProfileScreen(),
},
);
}
}

4. Deferred Deep Linking​

Deferred deep linking allows you to deep link users to specific content even if they haven't installed your app yet. When a user clicks your link, gets redirected to the app store, installs your app, and opens it for the first time — the original deep link data is preserved and delivered to your app.

How It Works​

  1. User clicks link → App not installed
  2. Ulinkly stores → Click data + device fingerprint
  3. User redirected → To App Store / Play Store
  4. User installs → Downloads and installs app
  5. User opens app → First launch triggers initialize() or checkDeferredLink()
  6. SDK matches → Finds original click data
  7. App receives → Original link data via onDynamicLink stream
  8. User navigates → To the intended content

By default, the SDK automatically checks for deferred links on first app launch:

await ULink.instance.initialize(
ULinkConfig(
apiKey: 'your-api-key',
autoCheckDeferredLink: true, // Default: true
),
);

// Deferred links are delivered via the same streams as regular links
_sdk.onDynamicLink.listen((linkData) {
if (linkData.isDeferred) {
debugPrint('This is a deferred deep link!');
debugPrint('Match type: ${linkData.matchType}');
}
_handleLink(linkData);
});
PropertyTypeDescription
isDeferredbooltrue if this link came from deferred deep linking
matchTypeString?How the link was matched: "deterministic" (Install Referrer) or "fingerprint" (probabilistic fingerprint match)

Initialize during app startup​

Initialize on both platforms during normal app startup to receive cold-start and deferred links. Ordinary remote HTTPS requests do not trigger iOS Local Network permission. Delaying SDK initialization until after onboarding can miss the first launch's link context.

void main() async {
WidgetsFlutterBinding.ensureInitialized();
await ULink.instance.initialize(
ULinkConfig(apiKey: 'your-api-key', autoCheckDeferredLink: true),
);
runApp(const MyApp());
}

Set autoCheckDeferredLink: false only if your app intentionally runs the check later; it is not an iOS permission workaround. Matching still requires connectivity and an eligible click.

Matching Methods​

The SDK uses two methods to match deferred links:

MethodPlatformAccuracyDescription
Install ReferrerAndroid onlyDeterministic when availableUses Google Play's Install Referrer API to get the exact click ID
Fingerprint MatchingiOS & AndroidProbabilistic, estimated 70–95%; not guaranteedMatches the IP address, a screen-width bucket, and the timezone within 24 hours of the click

On Android, the SDK automatically uses Install Referrer when available, falling back to fingerprint matching if needed.

Important Notes​

  1. One-Time Check: Deferred link check only runs once per app installation. Subsequent calls to checkDeferredLink() are ignored.

  2. 24-Hour Expiry: Deferred link data expires 24 hours after the original click.

  3. Network Required: The check requires an active network connection.

  4. Fresh Install Only: Deferred links only work on fresh installs, not app updates.

5. Integration with Routing Packages​

Using go_router​

import 'package:go_router/go_router.dart';

void _handleLink(ULinkResolvedData data) {
final parameters = data.parameters;
if (parameters != null) {
final screen = parameters['screen'] as String?;
switch (screen) {
case 'product':
final productId = parameters['productId'] as String?;
if (productId != null) {
context.go('/product/$productId');
}
break;
case 'profile':
final userId = parameters['userId'] as String?;
if (userId != null) {
context.go('/profile/$userId');
}
break;
}
}
}

Using auto_route​

import 'package:auto_route/auto_route.dart';

void _handleLink(ULinkResolvedData data) {
final parameters = data.parameters;
if (parameters != null) {
final screen = parameters['screen'] as String?;
switch (screen) {
case 'product':
final productId = parameters['productId'] as String?;
if (productId != null) {
context.router.push(ProductRoute(productId: productId));
}
break;
}
}
}

Best Practices​

  • Always check for initial links: Use getInitialDeepLink() when your app starts to handle links that launched the app
  • Cancel subscriptions: Always cancel stream subscriptions in dispose() to prevent memory leaks
  • Handle both stream types: Set up listeners for both onDynamicLink and onUnifiedLink
  • Validate parameters: Always validate and safely access parameters before using them
  • Handle errors: Add onError callbacks to stream listeners to handle errors gracefully
  • Centralize navigation logic: Create a single method to handle all link-based navigation
  • Handle edge cases: Account for missing parameters or invalid link data
  • Provide fallbacks: If navigation fails, show an appropriate error or fallback screen
  • Use routing packages: Consider using packages like go_router or auto_route for better navigation management

State Management​

  • Update UI reactively: Use setState or state management solutions to update UI when links are received
  • Store link data: Consider storing link data in your state management solution if needed across the app
  • Handle app lifecycle: Check for initial and last links when the app starts

Troubleshooting​

  • Verify URL scheme is configured in iOS (Info.plist)
  • Check Associated Domains are properly set up in iOS
  • Verify intent filters are configured in AndroidManifest.xml
  • Ensure domain matches dashboard configuration
  • Test on both platforms separately

Streams Not Emitting​

  • Confirm SDK initialization completed successfully
  • Check that streams are set up after SDK initialization
  • Verify native platforms are properly configured
  • Enable debug mode to see processing logs
  • Check that DEBUG: Link listeners setup complete appears in logs

Parameters Missing​

  • Verify link was created with parameters
  • Check that you're listening to the correct stream (dynamic vs unified)
  • Ensure parameters are accessed safely with null checks
  • Check the resolved data structure in debug logs

Query-Parameter Passthrough​

If a link was created with allowQueryPassthrough: true, any query params appended to the link URL (e.g. ?orderId=123) are merged into data.parameters and delivered alongside stored parameters. Passthrough values always arrive as strings and override a stored param with the same key.

void _handleDynamicLink(ULinkResolvedData data) {
// Works the same whether the value came from stored params or passthrough
final orderId = data.parameters?['orderId'] as String?;
if (orderId != null) {
navigateToOrder(orderId);
}
}

Passthrough works on direct opens (Universal Links on iOS, App Links on Android, and custom schemes) and after a deferred install. No SDK code change is needed. See the Query-Parameter Passthrough guide for limits and setup.

Testing Tips​

iOS Testing​

  • Use the Notes app or Safari to open your Ulinkly
  • Test both custom schemes and universal links
  • Check Console.app for link processing logs

Android Testing​

adb shell am start \
-a android.intent.action.VIEW \
-d "https://links.shared.ly/slug"
  • Verify Logcat contains link processing messages
  • Test both custom schemes and App Links

General Testing​

  • Confirm DEBUG: Link listeners setup complete appears before tapping the link
  • Test cold starts (app launched from link)
  • Test warm starts (app already running)
  • Verify streams emit data when links are tapped

Next Steps​