DeeplinkServiceProvider
On this page
Introduction#
DeeplinkServiceProvider is the bootstrap entry point for the magic_deeplink plugin. It wires together the platform driver, the singleton manager, and optional third-party handlers (OneSignal) using the Magic Framework's IoC container and two-phase provider lifecycle.
The provider lives at lib/src/providers/deeplink_service_provider.dart and extends ServiceProvider from package:magic/magic.dart.
Two-Phase Bootstrap#
The Magic Framework calls providers in two ordered phases:
| Phase | Method | Constraint |
|---|---|---|
| 1 | register() |
Sync. Only bind into the container — no other service may be accessed yet. |
| 2 | boot() |
Async. All providers have been registered. Safe to resolve, configure, and wire services. |
Splitting into two phases guarantees that when boot() runs, every binding registered by every other provider is already resolvable from the container.
Register Phase#
@override
void register() {
app.singleton('deeplinks', () => DeeplinkManager());
}
register() binds a single singleton into the container under the key 'deeplinks'. DeeplinkManager uses the standard singleton factory pattern (factory DeeplinkManager() => _instance), so the closure and the factory both guarantee one shared instance exists for the lifetime of the app.
Nothing else happens here. Config is not read, the driver is not created, and no other service is accessed — all of that is deferred to boot().
Boot Phase#
boot() is async. It resolves the already-registered manager and config, then sets up the driver, the live-link stream, the initial link, and the optional OneSignal handler.
@override
Future<void> boot() async {
final config = app.make<ConfigRepository>('config');
final driverName = config.get('deeplink.driver');
final manager = app.make<DeeplinkManager>('deeplinks');
// ... driver and handler wiring
}
Driver Initialization#
The provider reads deeplink.driver from config and creates the matching platform driver. Currently the only supported value is 'app_links'.
if (driverName == 'app_links') {
final driver = AppLinksDriver();
manager.setDriver(driver);
await driver.initialize(config.get('deeplink') ?? {});
// ...
}
driver.initialize() is awaited — it must complete before the stream is wired or the initial link is fetched. The entire deeplink config map is passed to the driver so it can read any platform-specific keys it requires.
Stream Wiring#
After initialization the driver's onLink stream is forwarded directly to manager.handleUri():
driver.onLink.listen((uri) {
manager.handleUri(uri);
});
This connects the platform event source to the handler chain. Every URI the driver emits — Universal Links on iOS, App Links on Android — is dispatched through manager.handleUri(), which fans the URI out to all registered handlers and emits it on manager.onLink.
Initial Link Handling#
When the app is cold-started from a deep link, the platform holds the URI until it is explicitly fetched. Fetching it synchronously during boot is unsafe because the app's router is not yet mounted at that point — it finishes initializing after runApp() returns, which happens after all providers have booted.
The provider defers the fetch to the next microtask / event-loop turn using Future.delayed(Duration.zero, ...):
Future.delayed(Duration.zero, () async {
final uri = await manager.getInitialLink();
if (uri != null) {
manager.handleUri(uri);
}
});
Duration.zero schedules the callback after the current frame completes, ensuring the router (and any registered handlers) are fully ready before the initial link is dispatched. manager.getInitialLink() caches its result, so repeated calls are safe.
OneSignal Handler Setup#
If the magic_notifications plugin is present and bound, the provider attaches a OneSignalDeeplinkHandler that listens for tapped-push events and converts them to deep link URIs:
if (app.bound('notifications')) {
final handler = OneSignalDeeplinkHandler();
try {
handler.setup(manager, app.make('notifications'));
_pushClicks = handler;
} catch (error) {
handler.dispose();
if (Magic.bound('log')) {
Log.error(
'[deeplink] Resolving the bound `notifications` manager failed, so '
'a tapped push notification cannot open a deep link: $error',
);
}
}
}
The handler is HELD rather than discarded, so dispose() can reach it.
setup() takes the notification MANAGER, not a stream, and subscribes to its onPushClicked stream, which the manager owns from construction and republishes onto whenever a driver is attached later. That is what makes this independent of provider order: only the 'notifications' binding needs to exist at this point, never a resolved push driver, because every provider has registered by the time any of them boots. OneSignalDeeplinkHandler then extracts a URI from the notification payload (checking keys url, deep_link, link, uri) and calls manager.handleUri() for any non-null result.
Optional Dependency Handling#
The integration with magic_notifications is entirely optional. The pattern used has three layers of defence:
-
app.bound('notifications')— guards the entire block. If the plugin was never registered, the block is skipped without error. -
dynamicresolution inside the handler:app.make('notifications')is handed to the handler untyped, andOneSignalDeeplinkHandlerreadsonPushClickedoff it structurally rather than importingmagic_notificationsand naming its type. This keeps magic_deeplink free of a hard package dependency. When the bound manager is too old to publishonPushClicked, or publishes something that is not aStream, the handler reports the mismatch through magic'sLog(guarded byMagic.bound('log')) instead of throwing, so a notifications version mismatch degrades deep linking rather than breaking app boot. -
A
try/catcharound the resolution, reporting at error level through the sameLogseam.app.make('notifications')runs the binding factory andonPushClickedis a getter, so either can throw, and the handler only answersNoSuchMethodErrorby name (that one means "this build is too old"). Anything else, aStateErrorout of an uninitialised manager for instance, would otherwise escape. That matters more here than it looks: magic'sApplication.bootawaits providers in a bare loop with no error handling of its own, so an escaping throw does not degrade deep linking, it aborts app boot and every provider registered after this one, over a plugin that is optional by design.
This is not a swallowing catch. The wiring this replaced had a catch (e) whose body was two comment lines, and that is why the feature stayed inert across two years of releases. This one names what failed, then lets boot continue.
This pattern should be followed whenever magic_deeplink optionally integrates with another plugin.
Provider Teardown#
DeeplinkServiceProvider.dispose() drops everything boot() wired: the push-click handler, the driver's link subscription, the driver itself, and the manager's own reference to it (DeeplinkManager.forgetDriver(), which the manager has always exposed and nothing called). The scheduled initial-link read checks a disposed flag on both sides of its await, since a Future.delayed offers no handle to cancel.
await provider.dispose();
It is provider-level rather than handler-level on purpose. doc/basics/handlers.md tells a consumer to call teardown from their service provider, so a dispose() reaching only the push handler would read as tearing the provider down while leaving the driver running. It is also idempotent, because a consumer calling it does not know which parts a given deployment actually wired.
Registering the Provider#
Add the provider to the providers list in your app's config/app.dart:
import 'package:magic_deeplink/magic_deeplink.dart';
Map<String, dynamic> get appConfig => {
'app': {
'providers': [
// ... other providers
(app) => DeeplinkServiceProvider(app),
],
},
'deeplink': {
'driver': 'app_links',
// platform-specific keys passed through to driver.initialize()
},
};
The provider's position relative to any provider registering 'notifications' does not matter. app.bound('notifications') is checked during boot, after every provider has registered, and the OneSignal handler subscribes to the notification manager's own stream rather than to a driver, so it does not need that provider to have booted yet either.
Related#
- DeeplinkManager — singleton manager: handler chain, stream, initial link cache
- Drivers — platform driver wrapping the
app_linkspackage - Handlers — notification-to-URI bridge
- Magic Framework — Service Providers — two-phase lifecycle reference