Skip to content
Dashboard

Installation

Detour provides a Flutter plugin (detour_flutter_plugin) that bridges the native Detour SDKs on Android and iOS, so both builds share one Dart integration.

A Detour app in the Dashboard is required before the SDK can resolve links. See Getting Started.

  • Flutter: >=3.3.0
  • Dart: ^3.11.1
  • Android min SDK: 24
  • iOS: 13.0+

Links are processed on Android and iOS only. On web, macOS, Linux, and Windows the plugin loads a no-op so a multi-platform codebase keeps building. See Platform support.

Add the package to your pubspec.yaml:

pubspec.yaml
dependencies:
detour_flutter_plugin: ^1.2.0

Then run:

Terminal window
flutter pub get

detour_flutter_plugin wraps the native Detour SDKs:

  • The Android bridge depends on com.swmansion.detour:detour-sdk:1.1.0.
  • The iOS bridge depends on the Detour CocoaPod (~> 1.1.0), pulled in automatically by the plugin podspec.

Keep google() and mavenCentral() in your project repositories so Gradle can resolve the Android dependency.

After changing iOS dependencies, run:

Terminal window
cd ios
pod install
cd ..

Both values come from API configuration in the Dashboard: the appID and the publishable apiKey. See Getting Started.

Pass your credentials to DetourConfig:

const DetourConfig(
apiKey: '<REPLACE_WITH_YOUR_API_KEY>',
appID: '<REPLACE_WITH_APP_ID_FROM_PLATFORM>',
shouldUseClipboard: true,
linkProcessingMode: LinkProcessingMode.all,
)

Keep apiKey and appID in environment variables or another secure configuration layer.

The plugin wires up the bridge and runtime listeners, but your app still needs native deep-link registration. The Dashboard generates the Associated Domains and App Links configuration for verified HTTPS links. It does not generate custom-scheme registration.

Paste the generated App Links filter into the MainActivity declaration in android/app/src/main/AndroidManifest.xml:

android/app/src/main/AndroidManifest.xml
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop">
<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" />
<data android:host="<your-host>" />
<data android:pathPrefix="/<app-hash>" />
<!-- The Dashboard adds any required legacy short-link prefixes. -->
</intent-filter>
</activity>

Use the exact host and paths generated in App configuration. New short links reuse /<app-hash> and do not require another manifest entry or native build.

If your app is served from a custom domain, the Dashboard generates the filter for that domain. Assign the domain before you copy the values. Changing the host an app is assigned to later needs a new build. Do not add hosts it did not generate: on Android 11 and lower that breaks App Link verification for every host at once, see Verification.

Add a separate filter to the same activity only if you use links such as myapp://product/42:

android/app/src/main/AndroidManifest.xml
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="myapp" />
</intent-filter>

No Android callback override is required. The plugin registers itself as a Flutter NewIntentListener, processes the activity’s initial intent, and receives later intents from the Flutter embedding.

In Xcode, open the Runner target, select Signing & Capabilities, add the Associated Domains capability, and enter applinks:<your-host>. The resulting Runner.entitlements entry is:

ios/Runner/Runner.entitlements
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:<your-host></string>
</array>

With a custom domain, the Dashboard generates this entry for that domain too.

If you use scheme links, register the scheme under the Runner target’s Info → URL Types, or add it directly to ios/Runner/Info.plist:

ios/Runner/Info.plist
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>myapp</string>
</array>
</dict>
</array>

Flutter plugins, Detour included, receive lifecycle callbacks through the Flutter delegate chain. If you override an iOS deep-link callback, return the super result:

ios/Runner/AppDelegate.swift
override func application(
_ application: UIApplication,
continue userActivity: NSUserActivity,
restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void
) -> Bool {
return super.application(
application,
continue: userActivity,
restorationHandler: restorationHandler
)
}
override func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey: Any] = [:]
) -> Bool {
return super.application(app, open: url, options: options)
}

If your project defines a custom SceneDelegate, subclass FlutterSceneDelegate (recommended) or forward scene events through FlutterSceneLifeCycleProvider. An app using Flutter’s default scene delegate does not need a custom class or callback overrides.

If your FlutterSceneDelegate subclass overrides either link callback, call super so Flutter can forward it to Detour:

ios/Runner/SceneDelegate.swift
override func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
super.scene(scene, continue: userActivity)
}
override func scene(
_ scene: UIScene,
openURLContexts URLContexts: Set<UIOpenURLContext>
) {
super.scene(scene, openURLContexts: URLContexts)
}

After changing either native target, rebuild the app. A Dart hot reload does not apply entitlements, Info.plist, or manifest changes.

Reference implementations in the example app:

The plugin processes links on Android and iOS. On every other platform, that is web, macOS, Linux, and Windows, it loads a no-op implementation, so apps that target several platforms build and run without conditional imports:

  • configure(...) does nothing and logs Detour: current platform is not supported, skipping initialization.
  • resolveInitialLink() returns a result with processed: true and no link, so your startup flow continues normally.
  • processLink(...) returns a result with processed: false.
  • linkStream is an empty stream.
  • logEvent(...) and logRetention(...) do nothing.

Call the SDK unconditionally from shared code. The no-op keeps unsupported platforms safe.

  1. Wire up the startup and runtime flow in Usage.
  2. Start from the runnable app in Examples.
  3. Check the result. See Testing & Troubleshooting.