Skip to content

DeeplinkServiceProvider

View raw .md
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#

dart
@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.

dart
@override
Future<void> boot() async {
  final config = app.make<ConfigRepository>('config');
  final manager = app.make<DeeplinkManager>('deeplinks');

  if (config.get<bool>('deeplink.enabled') == false) {
    return;
  }

  final driverName = config.get<String>('deeplink.driver');
  final driver = driverName == 'app_links' ? _driverFactory() : null;

  // ... driver and handler wiring
}

An absent deeplink.enabled key means enabled, matching what doc/getting-started/configuration.md documents (Config.get<bool>('deeplink.enabled', true)); only an explicit false skips everything below, including the OneSignal handler setup further down. _driverFactory defaults to AppLinksDriver.new and is a constructor parameter so a test can substitute a fake; the real driver reads isSupported from the host platform, which a test cannot control.

Driver Initialization#

The provider reads deeplink.driver from config and creates the matching platform driver. Currently the only supported value is 'app_links'. The driver is only wired further when it also reports isSupported:

dart
if (driver != null && driver.isSupported) {
  manager.setDriver(driver);
  await driver.initialize(config.get<Map<String, dynamic>>('deeplink') ?? {});

  // ...
}

A driver that answers isSupported == false (the web arm of AppLinksDriver, unconditionally) is never handed to the manager and never initialized: doing so would leave manager.driver answering with something that can never deliver a link, and would carry a driver for a mechanism the platform does not have.

driver.initialize() is awaited: it must complete before the stream is wired. 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 subscribed, and each URI is delivered once the app's first frame has rendered:

dart
final firstFrame = WidgetsFlutterBinding.ensureInitialized().endOfFrame;

driver.onLink.listen((uri) {
  unawaited(_deliver(manager, uri, firstFrame));
});

_deliver awaits firstFrame, then calls manager.handleUri(uri, source: DeeplinkSource.osLink) inside a try/catch that reports a failure through magic's Log (guarded by Magic.bound('log')) rather than letting it escape as an unhandled async error. Every URI the driver emits, Universal Links on iOS and App Links on Android, is dispatched this way, fanning out to all registered handlers and emitting on manager.onLink.

When the app is cold-started from a deep link, app_links serves that link as the first emission of the same onLink stream the block above already subscribes to (its own documentation describes the stream as carrying "all events: initial link and further"), so there is exactly one delivery path rather than two. An earlier version of this provider also called manager.getInitialLink() separately; on Android that read the same cold-start link app_links had already put on the stream, so a single tap ran the whole handler chain twice. That separate call is gone. DeeplinkManager.getInitialLink() remains on the manager for a consumer that wants to ask directly, and nothing in the provider calls it.

Fetching the link synchronously during boot would be unsafe regardless: the app's router is not yet mounted at that point, and _deliver accounts for this by awaiting WidgetsFlutterBinding.ensureInitialized().endOfFrame, captured once before the subscription is created. endOfFrame is a stronger guarantee than the Future.delayed(Duration.zero, ...) this replaced: it names the event itself (the router finishes building while the first frame is being built) rather than guessing a duration, and it still resolves via SchedulerBinding's own scheduling even when nothing is actively drawing a frame, so a link delivered to an idle application is not left waiting for a frame that never comes.

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:

dart
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(uri, source: DeeplinkSource.push, payload: data) for any non-null result, carrying the whole push payload with it.

Optional Dependency Handling#

The integration with magic_notifications is entirely optional. The pattern used has three layers of defence:

  1. app.bound('notifications'): guards the entire block. If the plugin was never registered, the block is skipped without error.

  2. dynamic resolution inside the handler: app.make('notifications') is handed to the handler untyped, and OneSignalDeeplinkHandler reads onPushClicked off it structurally rather than importing magic_notifications and naming its type. This keeps magic_deeplink free of a hard package dependency. When the bound manager is too old to publish onPushClicked, or publishes something that is not a Stream, the handler reports the mismatch through magic's Log (guarded by Magic.bound('log')) instead of throwing, so a notifications version mismatch degrades deep linking rather than breaking app boot.

  3. A try/catch around the resolution, reporting at error level through the same Log seam. app.make('notifications') runs the binding factory and onPushClicked is a getter, so either can throw, and the handler only answers NoSuchMethodError by name (that one means "this build is too old"). Anything else, a StateError out of an uninitialised manager for instance, would otherwise escape. That matters more here than it looks: magic's Application.boot awaits 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). _deliver checks a _disposed flag on both sides of its await, since an in-flight delivery waiting on endOfFrame has no handle a teardown could cancel it through.

dart
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:

dart
import 'package:magic_deeplink/magic_deeplink.dart';

Map<String, dynamic> get appConfig => {
  'app': {
    'providers': [
      // ... other providers
      (app) => DeeplinkServiceProvider(app),
    ],
  },
  'deeplink': {
    'enabled': true,
    '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.

esc

Search index did not load.

No match for "".

Try a class like rounded-lg or a widget like WButton.