dynalinks 2.0.0
dynalinks: ^2.0.0 copied to clipboard
Dynalinks SDK for Flutter - Deferred deep linking and attribution for iOS and Android apps.
Dynalinks Flutter SDK #
The official Flutter SDK for Dynalinks - deferred deep linking and attribution for iOS and Android apps.
Features #
- Deferred Deep Linking: Track users who click links before installing your app
- Universal Links / App Links: Handle incoming deep links automatically
- Cross-Platform: Single API for both iOS and Android
- Type-Safe: Full Dart type safety with comprehensive error handling
Requirements #
- Flutter 3.44.0 or later (Dart 3.12 or later)
- iOS 16.0 or later
- Android API 21 or later
On iOS the plugin builds with either Swift Package Manager (the default from Flutter 3.44) or CocoaPods. Either way you must set your app's iOS deployment target to 16.0, because Flutter's default is lower and the build fails until you raise it - see iOS Setup. The plugin ships a privacy manifest declaring its data use, so it is picked up automatically by your app's privacy report.
Upgrading from 1.x? See Upgrading from 1.x.
Installation #
Add dynalinks to your pubspec.yaml:
dependencies:
dynalinks: ^2.0.0
Then run:
flutter pub get
iOS Setup #
-
Register your iOS app in the Dynalinks Console:
- Bundle Identifier (from Xcode project settings)
- Team ID (from Apple Developer account)
- App Store ID (from your app's App Store URL)
-
Configure Associated Domains in Xcode:
- Open your iOS project > Signing & Capabilities
- Add the "Associated Domains" capability
- Add your domain:
applinks:yourproject.dynalinks.app
-
Set the iOS deployment target to 16.0:
- Open your iOS project > Runner target > General > Minimum Deployments
- Set iOS to 16.0 or later
Flutter's default is lower, so a project created from the Flutter template fails to build until you raise it. Under Swift Package Manager the error reads:
The package product 'dynalinks' requires minimum platform version 16.0 for the iOS platform, but this target supports 13.0
See the iOS integration guide for detailed instructions.
UIScene life cycle: incoming links are delivered through both the app delegate and the scene delegate, so the plugin works whichever life cycle your app uses. If your app has not adopted the UIScene life cycle yet,
flutter runmigrates it for you - Apple requires the adoption for apps built against the latest SDK.One detail of that migration matters for deep linking. Under UIScene, plugins must be registered from
didInitializeImplicitFlutterEngine, not fromdidFinishLaunchingWithOptions, because a link that launches the app is delivered while the scene connects. Registering later means the plugin is not listening yet when the launch link arrives. The automatic migration does this for you; if you maintainAppDelegate.swiftby hand, it should look like this:@main @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) } }
Android Setup #
-
Register your Android app in the Dynalinks Console:
- Package identifier (from
build.gradleapplicationId) - SHA-256 certificate fingerprint (run
./gradlew signingReport)
- Package identifier (from
-
Add the JitPack repository to
android/build.gradle.kts, where the Flutter template declares its repositories:
allprojects {
repositories {
google()
mavenCentral()
maven { url = uri("https://jitpack.io") }
}
}
If your project declares repositories in a dependencyResolutionManagement block in
android/settings.gradle.kts instead, add it there.
- Add intent filter to your
AndroidManifest.xml:
Add the intent filter to your existing MainActivity - leave the rest of the activity as Flutter
generated it:
<activity android:name=".MainActivity">
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="yourproject.dynalinks.app" />
</intent-filter>
</activity>
See the Android integration guide for detailed instructions.
Gradle: the plugin uses Flutter's built-in Kotlin support and no longer applies the Kotlin Gradle Plugin itself, so it builds under AGP 9. It compiles against Java 17; if your app pins an older JVM target you may need to raise it.
Usage #
Initialize the SDK #
Configure the SDK as early as possible in your app's lifecycle:
import 'package:flutter/material.dart';
import 'package:dynalinks/dynalinks.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Dynalinks.configure(
clientAPIKey: 'your-client-api-key',
logLevel: DynalinksLogLevel.debug, // Use .error in production
);
runApp(const MyApp());
}
Check for Deferred Deep Links #
Check if the user came from a Dynalinks link before installing:
Future<void> checkDeferredDeepLink() async {
try {
final result = await Dynalinks.checkForDeferredDeepLink();
if (result.matched && result.link != null) {
// User came from a deep link - navigate accordingly
final deepLinkValue = result.link!.deepLinkValue;
if (deepLinkValue != null) {
navigateTo(deepLinkValue);
}
}
} on SimulatorException {
// Running on simulator - deferred deep linking not available
} on DynalinksException catch (e) {
print('Error: ${e.message}');
}
}
Handle Incoming Deep Links #
Incoming links reach your app through two separate channels, and you need both:
getInitialLink()- the link that launched the app from a terminated state (cold start)onDeepLinkReceived- links that arrive while the app is already running (warm start)
A launch link is delivered only through getInitialLink(), even if a stream listener is already
attached. That is what keeps a single tap from being delivered twice - but it also means an app
that only subscribes to the stream will never see cold-start links.
A link that arrives while no listener is attached is held and delivered to the next one, so a tap during a route change is not lost. The SDK retains the 32 most recent queued links; if that limit is reached before a listener attaches, it discards the oldest link.
There is one exception to the rule above, and it exists so that no link is lost. getInitialLink()
answers once: if it has already told your app there is no link, a launch link that only resolves
afterwards - a very slow first request, or a configure() that failed and was retried - has
nowhere left to go, so it arrives on the stream instead. Handling both channels, as the example
below does, means you never have to think about it.
class _MyAppState extends State<MyApp> {
StreamSubscription<DeepLinkResult>? _subscription;
@override
void initState() {
super.initState();
_checkInitialLink();
_listenForLinks();
}
@override
void dispose() {
_subscription?.cancel();
super.dispose();
}
Future<void> _checkInitialLink() async {
// Check for cold start link
final initialLink = await Dynalinks.getInitialLink();
if (initialLink != null && initialLink.matched) {
_handleResult(initialLink);
}
}
void _listenForLinks() {
_subscription = Dynalinks.onDeepLinkReceived.listen(_handleResult);
}
void _handleResult(DeepLinkResult result) {
if (result.matched && result.link?.deepLinkValue != null) {
// Navigate to the deep link destination
Navigator.pushNamed(context, result.link!.deepLinkValue!);
}
}
}
Manual Link Resolution #
Manually resolve a URI if needed:
final result = await Dynalinks.handleDeepLink(
Uri.parse('https://yourproject.dynalinks.app/promo'),
);
if (result.matched) {
// Handle the resolved link
}
Upgrading from 1.x #
-
Raise your minimum SDK versions. 2.0.0 requires Flutter 3.44 / Dart 3.12, needed for both Swift Package Manager and the UIScene life cycle APIs.
-
Call
getInitialLink()on startup. A link that launched the app is now delivered only there, never on theonDeepLinkReceivedstream. If your 1.x app relied on the stream alone, it will silently stop receiving cold-start links. See Handle Incoming Deep Links for the pattern. -
iOS: run the app once to migrate.
flutter runadopts the UIScene life cycle and switches the project to Swift Package Manager for you. If you maintainAppDelegate.swiftby hand, apply the plugin-registration change shown in iOS Setup. -
iOS: update the native SDK. 2.0.0 requires Dynalinks iOS SDK 1.0.4 or later. On CocoaPods, run
pod update DynalinksSDK; Swift Package Manager resolves it automatically.
Nothing changed in the Dart API surface - no method signatures, parameters, or model fields were renamed or removed.
API Reference #
Dynalinks #
| Method | Description |
|---|---|
configure() |
Initialize the SDK with your API key |
checkForDeferredDeepLink() |
Check for deferred deep link (first launch) |
handleDeepLink(Uri) |
Manually resolve a deep link URI |
getInitialLink() |
Get the link that launched the app (cold start) |
onDeepLinkReceived |
Stream of incoming links while app is running |
reset() |
Reset SDK state. Annotated @visibleForTesting - calling it from app code triggers an analyzer warning |
version |
SDK version string |
DeepLinkResult #
| Property | Type | Description |
|---|---|---|
matched |
bool |
Whether a link was matched |
confidence |
Confidence? |
Match confidence (high/medium/low) |
matchScore |
int? |
Match score (0-100) |
link |
LinkData? |
The matched link data |
isDeferred |
bool |
Whether from deferred deep link |
LinkData #
| Property | Type | Description |
|---|---|---|
id |
String |
Unique link identifier |
name |
String? |
Link name (for display) |
path |
String? |
Link path |
shortenedPath |
String? |
Shortened path |
url |
Uri? |
Original URL the link points to |
fullUrl |
Uri? |
Full Dynalinks URL |
deepLinkValue |
String? |
Value for in-app navigation |
iosDeferredDeepLinkingEnabled |
bool? |
Whether iOS deferred deep linking is enabled |
iosFallbackUrl |
Uri? |
iOS fallback URL (when app not installed) |
androidFallbackUrl |
Uri? |
Android fallback URL (when app not installed) |
enableForcedRedirect |
bool? |
Whether forced redirect is enabled |
socialTitle |
String? |
Social sharing title |
socialDescription |
String? |
Social sharing description |
socialImageUrl |
Uri? |
Social sharing image |
clicks |
int? |
Number of clicks on this link |
referrer |
String? |
Referrer tracking parameter for attribution |
providerToken |
String? |
Apple Search Ads attribution token (pt) |
campaignToken |
String? |
Campaign identifier for attribution (ct) |
Exceptions #
| Exception | Description |
|---|---|
NotConfiguredException |
SDK not configured |
InvalidApiKeyException |
Invalid API key |
SimulatorException |
Running on simulator/emulator |
NetworkException |
Network request failed |
InvalidResponseException |
Server returned invalid response |
ServerException |
Server returned an error |
NoMatchException |
No matching link found |
InvalidIntentException |
Invalid intent data (Android) |
InstallReferrerUnavailableException |
Install Referrer API unavailable (Android) |
InstallReferrerTimeoutException |
Install Referrer connection timed out (Android) |
UnknownException |
Unknown error occurred |
Configuration Options #
await Dynalinks.configure(
clientAPIKey: 'your-api-key', // Required
baseURL: 'https://custom.api.url', // Optional, custom API URL
logLevel: DynalinksLogLevel.debug, // Optional, default: .error
allowSimulatorOrEmulator: false, // Optional, default: false
);
Log Levels #
DynalinksLogLevel.none- No loggingDynalinksLogLevel.error- Errors only (default)DynalinksLogLevel.warning- Warnings and errorsDynalinksLogLevel.info- Info, warnings, and errorsDynalinksLogLevel.debug- All logs
Attribution Tracking #
The SDK provides attribution data for campaign tracking and analytics:
final result = await Dynalinks.checkForDeferredDeepLink();
if (result.matched && result.link != null) {
final link = result.link!;
// Track attribution data for analytics
if (link.referrer != null) {
print('Referrer: ${link.referrer}'); // e.g., "utm_source=facebook&utm_campaign=summer"
}
if (link.providerToken != null) {
print('Apple Search Ads token: ${link.providerToken}'); // pt parameter
}
if (link.campaignToken != null) {
print('Campaign: ${link.campaignToken}'); // ct parameter
}
// Send to your analytics platform
analytics.track('deep_link_opened', properties: {
'referrer': link.referrer,
'provider_token': link.providerToken,
'campaign': link.campaignToken,
'deep_link': link.deepLinkValue,
});
}
Example App #
See the example directory for a complete sample app demonstrating all SDK features.
Support #
License #
This project is licensed under the MIT License - see the LICENSE file for details.