Drivers
A driver turns a platform's link mechanism into a uniform Stream<Uri> for DeeplinkManager, and AppLinksDriver is the one that ships with the package.
On this page
Quick Reference#
| Platform | AppLinksDriver.isSupported |
Mechanism |
|---|---|---|
| Android | true |
App Links (HTTPS intent filter) |
| iOS | true |
Universal Links (apple-app-site-association) |
| macOS | true |
Universal Links; the install and the doctor do not cover macOS |
| Web | false |
None; see Web, in detail |
| Windows, Linux | false |
None |
DeeplinkServiceProvider creates AppLinksDriver when deeplink.driver is 'app_links', and wires it only when isSupported is true. You rarely touch the driver directly.
The DeeplinkDriver Contract#
Every driver extends DeeplinkDriver:
abstract class DeeplinkDriver {
String get name;
bool get isSupported;
Future<void> initialize(Map<String, dynamic> config);
Future<Uri?> getInitialLink();
Stream<Uri> get onLink;
void dispose();
}
| Member | Contract |
|---|---|
name |
A stable identifier. AppLinksDriver answers 'app_links'. |
isSupported |
Whether the driver can run on this platform. Synchronous. The provider skips initialize, the manager and the subscription for a driver of its own that answers false. |
initialize |
Called once by the provider with the whole deeplink config map, before onLink is read. |
getInitialLink |
The URI that cold-started the app, or null. Called by DeeplinkManager.getInitialLink(), which caches the answer; the provider does not call it. Return null on failure rather than throwing. |
onLink |
A stream of every URI received while the app runs. Return Stream.empty() where the platform has none. |
dispose |
Releases what the driver holds. DeeplinkServiceProvider.dispose() calls it for a driver the provider created. |
AppLinksDriver#
The exported AppLinksDriver is a conditional-export barrel over platform arms chosen at compile time:
export 'app_links_driver_stub.dart'
if (dart.library.js_interop) 'app_links_driver_web.dart'
if (dart.library.io) 'app_links_driver_io.dart';
The dart:io arm wraps the app_links package and is the only arm that talks to a platform channel. It answers isSupported from Platform.isAndroid || Platform.isIOS || Platform.isMacOS, creates its AppLinks instance in initialize, and takes getInitialLink() and onLink from app_links. The cold-start link is also the first emission of onLink.
The web arm and the stub arm, which no shipped build target resolves to, are inert: isSupported is false, getInitialLink() returns null, onLink is Stream<Uri>.empty(), and initialize and dispose do nothing.
import 'package:magic_deeplink/magic_deeplink.dart';
Future<void> main() async {
final AppLinksDriver driver = AppLinksDriver();
await driver.initialize({});
final Uri? initial = await driver.getInitialLink();
if (initial != null) {
// The link that launched the app.
}
driver.onLink.listen((Uri uri) {
// Links received while the app runs.
});
}
Platform Support#
On Android, iOS and macOS the barrel resolves to the dart:io arm. On Windows and Linux it resolves to the same arm, whose Platform check answers false, so the provider wires nothing there either. Native setup per platform is in Installation.
Web, in detail#
"No driver" is not "no deep links". The web has routes into a screen that this package owns no driver for, so both are easy to leave half configured.
A tapped push, with the tab open, reaches the handler chain. The push bridge is wired outside the isSupported gate in DeeplinkServiceProvider.boot(), so it exists on the web as on mobile: OneSignalWebDriver publishes the click, the notification manager republishes it, and OneSignalDeeplinkHandler routes it with DeeplinkSource.push. Put the link in the notification's additionalData under url, deep_link, link or uri, the keys mobile reads. A launch URL set on the OneSignal side alone is not read.
An address-bar link is GoRouter's job. The driver is inert rather than wired to app_links_web, because that package reads location.href once at boot and never reacts to later navigation, so routing the same URI through both would duplicate it. GoRouter only receives a clean path when both of these outside this package hold, and neither fails loudly:
routing.url_strategyis'path'in the app's routing config. Without it Flutter uses the hash strategy andhttps://app.example.com/incidents/5is not a route. See Routing in Magic.- The web host rewrites unknown paths to
index.html. Without it the same URL is a plain 404 from the host and Flutter never boots.
A push clicked with no tab open runs no Dart code, so nothing in this package reads additionalData; whatever page the browser then opens is an ordinary page load and depends on the two points above.
Custom Drivers#
A custom driver extends DeeplinkDriver and implements every member. The provider wires only the built-in driver, so a custom one does not feed the manager on its own: manager.setDriver() makes it the manager's driver (what manager.driver and getInitialLink() use), and you call initialize, subscribe to onLink and pass each URI to handleUri yourself.
import 'dart:async';
import 'package:magic/magic.dart';
import 'package:magic_deeplink/magic_deeplink.dart';
class QrDriver extends DeeplinkDriver {
final StreamController<Uri> _controller = StreamController<Uri>.broadcast();
@override
String get name => 'qr';
@override
bool get isSupported => true;
@override
Future<void> initialize(Map<String, dynamic> config) async {}
@override
Future<Uri?> getInitialLink() async => null;
@override
Stream<Uri> get onLink => _controller.stream;
/// Call this from the scanner callback.
void emit(Uri uri) => _controller.add(uri);
@override
void dispose() => unawaited(_controller.close());
}
class AppServiceProvider extends ServiceProvider {
AppServiceProvider(super.app);
@override
void register() {}
@override
Future<void> boot() async {
final DeeplinkManager manager = DeeplinkManager();
final QrDriver driver = QrDriver();
await driver.initialize(<String, dynamic>{});
manager.setDriver(driver);
driver.onLink.listen((Uri uri) {
unawaited(manager.handleUri(uri, source: DeeplinkSource.manual));
});
}
}
setDriver() replaces the previous driver without disposing it. MagicRoute.to throws until the router exists, which happens while the first frame is built, so a driver whose links can arrive that early should wait the way the provider does: capture WidgetsFlutterBinding.ensureInitialized().endOfFrame once and await it before handleUri.
Gotchas#
| Mistake | Fix |
|---|---|
| Expecting address-bar links on the web to reach the handlers. | They never do; GoRouter owns them. Set routing.url_strategy to 'path' and make the host rewrite unknown paths to index.html. |
Calling setDriver() with a custom driver and waiting for links. |
The provider wires only AppLinksDriver. Subscribe to the custom driver's onLink and call handleUri yourself. |
Setting deeplink.driver to any value other than 'app_links'. |
The provider creates no driver and no subscription, though the push bridge still runs. Keep 'app_links'. |
Routing the result of getInitialLink() as well as listening to onLink. |
The cold-start link is already the first emission of onLink, so the handler chain runs twice for one tap. Use one path. |
| Expecting links on Windows or Linux. | isSupported is false there and nothing is wired. |
| Assuming the install and the doctor set up macOS. | They cover iOS and Android only. Add the macOS entitlement by hand and verify on a device. |
Related Documentation#
- Handlers: what runs after a driver emits a link.
- Configuration: the
driverandenabledkeys. - Service Provider: boot order, driver initialisation and stream wiring.
- DeeplinkManager:
setDriver(),handleUri()and the cached initial link.