Installation
On this page
Introduction#
magic_deeplink adds Universal Links (iOS) and App Links (Android) to your Magic application. It follows the same ServiceProvider + driver + handler pattern used throughout the framework, so it wires up in exactly the same way as every other Magic plugin.
Under the hood the package delegates all platform stream handling to app_links on Android, iOS and macOS; there is no native Dart plugin code of its own. That does not remove the platform setup an operating system requires before it will hand your app a link at all: an iOS entitlement, an Android intent filter, and a switch on each platform that keeps Flutter's own deep link handler out of the way. See Platform Setup below.
Requirements#
- Dart SDK: 3.6.0 or higher
- Flutter: 3.27.0 or higher
- Magic Framework installed and bootstrapped (
lib/config/app.dartpresent)
Installing the Package#
Add magic_deeplink to your Flutter project:
flutter pub add magic_deeplink
Or add it manually to pubspec.yaml:
dependencies:
magic_deeplink: ^0.1.8
Then fetch dependencies:
flutter pub get
Running the Install Command#
The command is host-dispatched via fluttersdk_artisan (add MagicDeeplinkArtisanProvider to your app's lib/config/artisan.dart, see CLAUDE.md). Run it from your project root to scaffold the configuration file and inject the provider automatically:
dart run <app>:artisan deeplink:install
The command performs the following steps:
- Validates that
lib/config/app.dartexists (Magic must be installed first). - Creates
lib/config/deeplink.dartwith sensible defaults. - Injects
DeeplinkServiceProviderinto theproviderslist inlib/config/app.dart. - Injects
() => deeplinkConfiginto theconfigFactorieslist inlib/main.dart.
dart run <app>:artisan deeplink:install --force
Registering the Service Provider#
If you ran dart run <app>:artisan deeplink:install, the provider was already injected. The relevant section of lib/config/app.dart will look like this:
import 'package:magic/magic.dart';
import 'package:magic_deeplink/magic_deeplink.dart'; // injected by install
final appConfig = {
'app': {
'name': Env.get('APP_NAME', 'My App'),
'providers': [
(app) => RouteServiceProvider(app),
(app) => AppServiceProvider(app),
(app) => DeeplinkServiceProvider(app), // injected by install
],
},
};
Injecting the Config Factory#
The install command also adds the deeplink config factory to Magic.init() in lib/main.dart:
import 'package:flutter/material.dart';
import 'package:magic/magic.dart';
import 'config/app.dart';
import 'config/deeplink.dart'; // injected by install
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Magic.init(
configFactories: [
() => appConfig,
() => deeplinkConfig, // injected by install
],
);
runApp(MagicApplication(title: 'My App'));
}
Registering a Route Handler#
The install command wires the driver and the provider, but it registers no handler: DeeplinkServiceProvider.boot() never constructs a RouteDeeplinkHandler on its own, because it has no way to know which paths your app claims. Register one yourself, in your own service provider's boot(), after the paths in your config are settled:
import 'package:magic/magic.dart';
import 'package:magic_deeplink/magic_deeplink.dart';
class AppServiceProvider extends ServiceProvider {
@override
Future<void> boot() async {
DeeplinkManager().registerHandler(
RouteDeeplinkHandler(
paths: app.make<ConfigRepository>('config').get('deeplink.paths'),
),
);
}
}
See Handlers for the full contract and for writing a custom handler.
Configuration Reference#
The generated lib/config/deeplink.dart looks like this out of the box:
Map<String, dynamic> get deeplinkConfig => {
'deeplink': {
'enabled': true,
'driver': 'app_links', // only supported driver
'domain': 'example.com', // your Universal Link / App Link domain
'scheme': 'https',
'ios': {
'team_id': 'YOUR_TEAM_ID', // Apple Developer Team ID
'bundle_id': 'com.example.app', // app bundle identifier
},
'android': {
'package_name': 'com.example.app',
'sha256_fingerprints': [
'YOUR_SHA256_FINGERPRINT', // keystore SHA-256, colon-separated
],
},
'paths': [
'/*', // path patterns handled as deep links
],
},
};
Replace example.com, the team/bundle identifiers, and the SHA-256 fingerprint with your real values before deploying.
Platform Setup#
An operating system will not hand your app a link until you tell it your app owns the domain, and Flutter's own deep link handler will race the app_links driver this package wires in unless you switch it off. Neither step is automated beyond the one plist key noted below; both are one-time edits to the native project.
iOS: Associated Domains and Info.plist#
-
Add the Associated Domains capability. In Xcode, open the
Runnertarget's Signing & Capabilities tab, add "Associated Domains", and add an entry of the formapplinks:<your-domain>(e.g.applinks:example.com). This sets thecom.apple.developer.associated-domainsentitlement:text <key>com.apple.developer.associated-domains</key> <array> <string>applinks:example.com</string> </array> -
Turn off Flutter's own deep link handler.
app_linksowns link delivery in this package; Flutter's default handler competing for the same link is whatflutter_deeplinking_enabledavoids on Android. On iOS the equivalent switch is theFlutterDeepLinkingEnabledkey inios/Runner/Info.plist, set tofalse:text <key>FlutterDeepLinkingEnabled</key> <false/>The install command applies this key for you (
install.yaml'snative.ios.info_plist), so a freshdeeplink:installrun needs no manual edit here. Re-check it if your project predates this release, or if something else in your project'sInfo.plisttemplate has since overwritten it. -
Host the
apple-app-site-associationfile. Generate it withdart run <app>:artisan deeplink:generate --output ./publicand upload it tohttps://<your-domain>/.well-known/apple-app-site-association, served over HTTPS with no redirect.
Android: Intent Filter and Manifest#
-
Add the intent filter. Inside the
<activity>element for.MainActivityinandroid/app/src/main/AndroidManifest.xml, add an autoVerify intent filter with a<data>element for bothhttpandhttps(Android's own guidance: "The intent filter must include<data>elements for bothhttpandhttpsschemes."):text <activity android:name=".MainActivity" android:exported="true" ...> <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="http" android:host="example.com" /> <data android:scheme="https" android:host="example.com" /> </intent-filter> </activity> -
Turn off Flutter's own deep link handler, inside
<activity>. Add this<meta-data>element inside the same<activity>element as the intent filter above, not inside<application>:text <meta-data android:name="flutter_deeplinking_enabled" android:value="false" />This one is not applied by
deeplink:install. Artisan'sXmlEditor(the shared engine every plugin'snative.android.meta_datakey runs through) inserts<meta-data>entries into<application>, and Flutter reads this specific key from<activity>; automating it through that key would silently write a<meta-data>entry Flutter never looks at, passing every check while doing nothing. Add it by hand. -
Host the
assetlinks.jsonfile. Generate it withdart run <app>:artisan deeplink:generate --output ./publicand upload it tohttps://<your-domain>/.well-known/assetlinks.json. Android verifies it at install time, not at link-click time.
Verify the install before you reach for a device#
dart run <app>:artisan deeplink:doctor
Every way of getting this install wrong is silent. A <meta-data> on the wrong element, a missing <data> scheme, an association file that names a different bundle, a provider that never reached lib/config/app.dart: none of them throws, none of them logs, and the only symptom is a link that opens the browser. The doctor reads the project's own files and says which prerequisite is actually in place, including the Dart wiring that makes every platform file matter.
Add --remote to also fetch both association files from the live domain, which is the half a repo-only check cannot see:
dart run <app>:artisan deeplink:doctor --remote
What it can never prove is that a real device matches an incoming link to this app: swcutil verify needs root and Android verifies at install time. The last mile is always a real device, and on iOS it must be a profile or release build, since iOS refuses to launch a debug Flutter build from a link or from the home screen.
Next Steps#
Now that the plugin is installed and wired up:
- Configuration: Learn how to customise paths, schemes, and platform settings.
- Handlers: Add your own URI handlers with
RouteDeeplinkHandler. - CLI Tools: Use
dart run <app>:artisan deeplink:generateto produce the Apple App Site Association and Android Asset Links files.