Skip to main content
The Userpilot Flutter plugin supports a custom URL scheme that lets you preview experiences on a real device before publishing them, and that carries push notification taps back into your app. Flutter has no built-in bridge for delivering a raw URL to Dart, so the setup has three parts: register the scheme with each OS, forward the URL from your platform code into Dart, then pass it to the SDK. The example app in the plugin repository implements one working version of this using its own method and event channels, which is the approach shown here.
Pass the whole URL, not just the pathThe SDK matches on the scheme and host, so it needs the complete URL. Forwarding only the path segment means no link is ever recognized.

Prerequisites

If your app already uses Flutter deep linking with the Router widget, disable Flutter’s automatic link handling so that you receive every link and decide where it goes. iOS — in Info.plist:
Android — in AndroidManifest.xml, on the main Activity:
You then process all links yourself and pass your app’s own links through to the Router system, as shown in Handle the custom URL scheme.

Register the custom URL scheme

Replace APP_TOKEN with your app’s Userpilot token, which you can obtain from your Environments Page. For example, if your token is NX-12345678, your scheme value is userpilot-nx-12345678. iOS — in Info.plist:
Android — in AndroidManifest.xml, on the main Activity:
Keep android:host="sdk" in this filter. Userpilot validates that incoming preview and push-notification URLs use the sdk host.
Which iOS setup you use depends on your Flutter version. The recommended one uses the UIScene lifecycle and needs Flutter 3.38.0 or later, because it relies on FlutterSceneDelegate and FlutterImplicitEngineDelegate, which were introduced in that release. Below 3.38.0, use the app-delegate setup. This affects only the integration code in your own app. The plugin itself works on any Flutter version its pubspec.yaml supports and requires no scene adoption.
First, register your scene delegate in Info.plist:
Under the scene lifecycle, your window and its FlutterViewController are created after application(_:didFinishLaunchingWithOptions:) returns. That shapes the code below in two ways: the launch URL is delivered to the scene rather than in launchOptions, and there is no binary messenger to build channels on until the Flutter engine exists. So the scene delegate captures the launch URL, and plugin registration and channel setup happen in didInitializeImplicitFlutterEngine, which Flutter calls once the engine is ready.SceneDelegate.swift:
AppDelegate.swift:
Both setups use the same LinkStreamHandler:
The queuedLinks array matters for cold starts: a link can arrive before your Dart code has subscribed to the event channel. Holding it until onListen runs means the link is delivered once Dart is listening, rather than dropped.

Migrating to the scene lifecycle

Earlier versions of this guide built both channels inside application(_:didFinishLaunchingWithOptions:) and read the launch URL from launchOptions[.url]. Neither works once your app adopts the scene lifecycle. To migrate:
  1. Add the UIApplicationSceneManifest entry shown above.
  2. Add SceneDelegate.swift, and add it to your Xcode target.
  3. Move plugin registration and channel setup out of didFinishLaunchingWithOptions and into didInitializeImplicitFlutterEngine.
  4. Delete your application(_:open:options:) override. Warm links now arrive through scene(_:openURLContexts:).
  5. Leave push notification handling in the AppDelegate. didRegisterForRemoteNotificationsWithDeviceToken and the notification-response callbacks still fire there.

MainActivity.kt:
onCreate captures the launch link and onNewIntent forwards links that arrive while the app runs, which mirrors the two iOS entry points.

Handle the custom URL scheme

In Dart, pass each incoming Uri to Userpilot.didHandleURL. It returns true when the SDK recognized and processed the link, and false when your app should route it, at which point you can hand the .path to your own RouteInformationParser.
Initialize the SDK before you consume the launch linkThe platform side holds the launch URL until your Dart code asks for it, so awaiting Userpilot.initialize first means the SDK can act on the link. didHandleURL returns false while the SDK is uninitialized, which sends the preview link to your own routing instead.
For more on navigation and routing in Flutter, see the Flutter navigation and routing example application.
The SDK only claims a URL when the scheme matches your token and the host is sdk. Every other URL is passed back to your app.
On iOS, notification taps are handled natively by the SDK and never reach your link handling, so follow Push Notifications for that setup.

Preview an experience with a QR code

Userpilot can render a draft experience on your device before you publish it, so you can check layout, copy, and behavior on a real screen. Preview is supported for Flows and Surveys. The preview is delivered over the custom URL scheme. It works once your app is installed on the device and the scheme is registered and handled as described above.
1

Open the experience

Open a Flow or Survey in the Experience Builder and enter Edit Mode.
2

Start the preview

Select Preview in the top-right corner of the builder. A popup with a QR code appears.
3

Scan the QR code

Scan the code with your device camera. Your app opens and the experience appears.
Points to keep in mind:
  • You don’t need to apply or save your changes before previewing them.
  • The experience doesn’t need to be live.
  • A preview bypasses the experience’s targeting and frequency settings, so it appears even when the current user wouldn’t normally qualify for it.
  • Once you close the experience, the app resumes normal behavior.

Test push notifications

You can send a test push to a specific user from the Userpilot dashboard and verify delivery and tap handling on a real device without sending anything to your wider audience. The notification doesn’t need to be live, so you can test it while it’s still a draft.
  • Test pushes work in both the staging and production environments.
  • Test pushes are not counted as real sends, and they’re excluded from analytics.
  • Test pushes bypass the notification’s targeting and frequency settings.
For the dashboard steps, see Mobile push notifications. Because a notification tap arrives as a deep link on Android, a test push also exercises your link forwarding there. If the notification appears but tapping it does nothing, the scheme registration is the first thing to check.
Open a link directly to confirm your scheme is registered and your handler runs, without going through the Builder:
Replace the scheme with your own token and 12345 with a real experience ID.

Troubleshooting

Confirm the registered scheme matches your token exactly, lowercased. A mismatched scheme means the OS never routes the link to your app. Verify the registration with the commands above.
You have the UIApplicationSceneManifest entry but still use the app-delegate code. Under the scene lifecycle, window is nil while didFinishLaunchingWithOptions runs, so window?.rootViewController as! FlutterViewController traps. Move to the Flutter 3.38.0 setup, or remove the manifest entry.
Flutter’s automatic deep link handling is intercepting the link. Set FlutterDeepLinkingEnabled to false on iOS and flutter_deeplinking_enabled to false on Android, as described in the prerequisites.
Each environment has its own token, and the scheme follows the token. Confirm the staging build registers the scheme for the token it initializes the SDK with.