Receive Links in Flutter
Handle incoming deep links in your Flutter app using the ULink SDK's reactive streams.
Prerequisites
Before receiving links, ensure you've completed the SDK setup:
SDK Integration
- Flutter SDK plugin added to your
pubspec.yamland installed ULink.instanceinitialized 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)
ULink Dashboard Configuration
- Project settings configured in the ULink 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.
Understanding Link Reception
The ULink 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.
1. Listen to Link 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(),
);
}
}
onDynamicLink: Emits dynamic links with parameters for in-app navigationonUnifiedLink: Emits unified links for marketing/redirect scenarios- Both streams emit
ULinkResolvedDataobjects containing the resolved link information
Always cancel stream subscriptions in dispose() to prevent memory leaks. The SDK uses broadcast streams, so multiple listeners can subscribe to the same stream.
2. Access Initial and Last Links
The SDK provides methods to access links that arrived before your app was ready or after a cold start.
Get Initial Deep Link
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);
}
}
Get Last Link Data
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);
}
}
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 ULink SDK
await ULink.instance.initialize(
ULinkConfig(
apiKey: 'your-api-key',
baseUrl: 'https://api.ulink.ly',
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
- User clicks link → App not installed
- ULink stores → Click data + device fingerprint
- User redirected → To App Store / Play Store
- User installs → Downloads and installs app
- User opens app → First launch triggers
initialize()orcheckDeferredLink() - SDK matches → Finds original click data
- App receives → Original link data via
onDynamicLinkstream - User navigates → To the intended content
Automatic Deferred Link Handling
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);
});
Link Data Properties for Deferred Links
| Property | Type | Description |
|---|---|---|
isDeferred | bool | true if this link came from deferred deep linking |
matchType | String? | How the link was matched: "deterministic" (Install Referrer) or "probabilistic" (fingerprint) |
On iOS, when your app makes its first network request, the system shows a permission dialog asking the user to allow network access. The problem is that the network request is already in flight when this dialog appears — the request will fail regardless of whether the user accepts or denies.
This means if autoCheckDeferredLink: true and the SDK initializes immediately, the deferred link check will fail before the user can even respond to the dialog.
Delayed Initialization (Recommended for iOS)
For iOS apps, delay SDK initialization entirely until after your onboarding flow or until after the user has made their first successful network request:
import 'dart:io';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// On Android, initialize immediately - no permission issues
if (Platform.isAndroid) {
await _initializeULink();
}
runApp(const MyApp());
}
Future<void> _initializeULink() async {
await ULink.instance.initialize(
ULinkConfig(
apiKey: 'your-api-key',
autoCheckDeferredLink: true, // Works fine on Android
),
);
}
// In your onboarding screen or after first successful network call:
class OnboardingScreen extends StatelessWidget {
void onOnboardingComplete() async {
// On iOS, initialize SDK only after onboarding
if (Platform.isIOS) {
await _initializeULink();
}
// Navigate to home screen
Navigator.of(context).pushReplacementNamed('/home');
}
}
- Don't initialize ULink immediately in
main() - Complete your app's onboarding or make another network request first (e.g., login, fetch config)
- Then call
ULink.instance.initialize()— by this point, iOS has already granted network permission - The deferred link check will now succeed and deliver via your
onDynamicLinklistener
Matching Methods
The SDK uses two methods to match deferred links:
| Method | Platform | Accuracy | Description |
|---|---|---|---|
| Install Referrer | Android only | 100% deterministic | Uses Google Play's Install Referrer API to get the exact click ID |
| Fingerprint Matching | iOS & Android | ~80-95% probabilistic | Matches based on device fingerprint, IP, and timing |
On Android, the SDK automatically uses Install Referrer when available, falling back to fingerprint matching if needed.
Important Notes
-
One-Time Check: Deferred link check only runs once per app installation. Subsequent calls to
checkDeferredLink()are ignored. -
24-Hour Expiry: Deferred link data expires 24 hours after the original click.
-
Network Required: The check requires an active network connection.
-
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
Link Handling
- 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
onDynamicLinkandonUnifiedLink - Validate parameters: Always validate and safely access parameters before using them
- Handle errors: Add
onErrorcallbacks to stream listeners to handle errors gracefully
Navigation
- 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_routerorauto_routefor better navigation management
State Management
- Update UI reactively: Use
setStateor 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
Links Not Opening App
- 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 completeappears 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
Testing Tips
iOS Testing
- Use the Notes app or Safari to open your ULink
- 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 completeappears 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
- Create Links → Flutter - Learn how to create links programmatically
- Troubleshoot & Test Deep Links - Debug and verify your links