deep_links_kit 0.1.0
deep_links_kit: ^0.1.0 copied to clipboard
One config for Android App Links and iOS Universal Links: a CLI that sets up the native projects and server files, plus router-agnostic link handling.
deep_links_kit #
One config for Android App Links, iOS Universal Links and custom URL schemes:
- A CLI that edits
AndroidManifest.xml, the iOS entitlements andInfo.plist, writesassetlinks.json/apple-app-site-associationfor your website, and checks that everything is served correctly. - A small runtime that receives links, waits until your app is ready (session loaded, navigator built), and opens the matching route with any router: go_router, Navigator 1.0, auto_route, and others.
Why #
Setting up deep links by hand means editing four native files and two server
files that must agree with each other, and then writing the same
DeepLinkService in every app. Common bugs this package prevents:
- The link opens twice, because Flutter's built-in deep linking and
app_linksboth handle it.setupturns the built-in one off. - The cold-start link is handled twice, because
app_linksreturns it fromgetInitialLink()and emits it onuriLinkStream. The runtime drops the duplicate. - Navigation runs before the user session is loaded, or before the navigator exists. The runtime waits for both.
- The website files miss a path or a fingerprint.
doctorfinds it.
1. Configure #
Add a section to your app's pubspec.yaml (or put the same keys in a
deep_links_kit.yaml file):
deep_links_kit:
hosts: [app.example.com] # App Links / Universal Links
scheme: myapp # myapp://... (optional)
paths: ["/share/*", "/p/*"] # optional, defaults to "/*"
android:
sha256: # for assetlinks.json and doctor
- "2E:3B:...:4A:C8"
ios:
team_id: ABCDE12345 # read from the Xcode project if omitted
| Key | Description |
|---|---|
hosts / host |
Web domains. *.example.com covers subdomains. |
schemes / scheme |
Custom URL schemes. |
paths |
Path globs on the hosts (* any characters, ? one). |
disable_flutter_deep_linking |
Default true. Keep it unless you let your router handle links itself. |
android.package |
Overrides applicationId from Gradle (flavors). |
android.sha256 |
Certificate fingerprints. |
ios.team_id / ios.bundle_id |
Override the values read from project.pbxproj. |
2. Run the CLI #
dart run deep_links_kit setup # --dry-run to preview
dart run deep_links_kit generate # --output <dir>, default build/deep_links_kit
dart run deep_links_kit doctor # --offline to skip the website checks
setup is safe to run again, and the config is the source of truth: values
you remove from it are removed from the native projects too. On Android it
writes one marked block inside the launcher activity and replaces that block
on later runs. On iOS it records what it added in deep_links_kit.lock
(commit this file), so a later run removes only its own old entries and never
another plugin's, such as a Facebook fb123 URL scheme. When the Runner
target has no entitlements file, setup creates Runner/Runner.entitlements
and links it in every build configuration.
doctor exits with code 1 when something will break deep links, so you can
run it in CI.
Android launchMode: with
singleTop(orstandard, the default), a link tapped in another app starts a second copy of your app: the splash runs again and in-memory state starts empty. Useandroid:launchMode="singleTask"on the launcher activity.setupanddoctorwarn about this but never change it, because it affects the whole app. The one trade-off: native screens opened on top of the activity (an image cropper, for example) close when a link arrives.Google Play: Play re-signs your app. Put the App signing key SHA-256 from Play Console > Test and release > App integrity in
assetlinks.json, not only your upload key.Apple: enable Associated Domains for the App ID in the Apple Developer portal. Apple caches
apple-app-site-associationon its CDN, so changes can take up to 24 hours (reinstall the app to retest).
3. Handle links #
final deepLinks = DeepLinksKit(
navigator: DeepLinkNavigator.custom(
push: router.push,
replace: router.go,
),
waitUntilReady: () => session.load(),
routes: [
DeepLinkRoute('/p/:id/:name?', to: (m) => '/profile/${m['id']}'),
DeepLinkRoute('/share/:type/:id', to: (m) => switch (m['type']) {
'service' => '/services/${m['id']}',
_ => null, // let the next route or the fallback decide
}),
],
fallback: (_) => '/home',
);
await deepLinks.init(); // as early as possible
Routers #
// go_router
DeepLinkNavigator.custom(push: router.push, replace: router.go);
// Navigator 1.0 named routes
DeepLinkNavigator.named(navigatorKey);
// Anything else
DeepLinkNavigator.custom(
push: (path) => myRouter.open(path),
isReady: () => myRouter.isMounted,
);
Route patterns #
Patterns match the link path. For custom schemes the host is part of the
path, so myapp://p/42 and https://app.example.com/p/42 both match /p/:id.
| Pattern | Matches | Captures |
|---|---|---|
/p/:id |
/p/42 |
m['id'] == '42' |
/p/:id/:name? |
/p/42, /p/42/ali |
optional trailing value |
/docs/* |
/docs, /docs/a/b |
m.rest == ['a', 'b'] |
Literal segments ignore letter case unless caseSensitive: true. Query
parameters are available as m.query. Set replace: true on a route to
replace the stack instead of pushing.
More #
handle(uri): route a link from somewhere else, such as a push notification.resolve(uri): get theDeepLinkTargetwithout navigating.hosts/schemes: ignore links for other domains or schemes.onLink/onError: logging and analytics hooks.source: pass your ownDeepLinkSourcein tests.
FAQ #
Can I change the domain without releasing a new build? No. Android and
iOS read the domains when the app is installed, so a new domain needs a new
build. Use a dedicated domain for links (for example links.example.com)
that never moves, even if your API does, or a wildcard host for subdomains.
How do I test?
adb shell pm verify-app-links --re-verify com.example.app
adb shell pm get-app-links com.example.app
adb shell am start -a android.intent.action.VIEW -d "https://app.example.com/p/1"
xcrun simctl openurl booted "https://app.example.com/p/1"
On a real iPhone, tap the link from Notes or Messages. Typing it in Safari's address bar does not open the app.