The missing guide to deep linking in Flutter apps - Part 1, Android
Google Developer Expert in Flutter & Dart 🚀| Mobile App Development 📱| Tech Advocate 🌐| Women Who Code KL Director
Search for a command to run...
Google Developer Expert in Flutter & Dart 🚀| Mobile App Development 📱| Tech Advocate 🌐| Women Who Code KL Director
No comments yet. Be the first to comment.
In this series, I will share about what I wish I had access to when I was implementing or troubleshooting something.
I've seen many different approaches on handling errors in Flutter projects, some even involves functional programming paradigm... Here I will show my take on how I handle errors while taking care of internationalisation (nope I'm not using easy_local...
I've been using Bloc for years. It was my go-to state management solution, and I was comfortable with it. Events go in, states come out. Simple, predictable, testable. So when people kept telling me to try Riverpod, I resisted. Why fix what isn't bro...
I've dealt with my share of performance issues in production Flutter apps. Janky scrolling that makes users think the app is broken. Memory leaks that crash the app after 10 minutes of use. The kind of problems that make you question your life choice...
I've been building Flutter apps for a team of around 5 developers for the past few years. When I started, everyone talked about Clean Architecture like it was the only "professional" way to build apps. So naturally, I tried it. After months of writin...
Recently I talked about how to perform automated end-to-end testing on any mobile apps (yes, any, even those you don’t own) with Maestro.Deck: https://excalidraw.com/#json=YBaTDvWm8yTdshL8Z2IOL,7h7736K_Ey7wg08QljkbPg
Recently I gave a talk at Google DevFest Georgetown 2024, about asynchronous Dart features (now with quizzes built in!). Here are the slides I promised:
This guide is meant to complement other resources out there. I will include the links to those resources useful to me in this article, but I would try not to repeat what was already out there (unless it was confusing).
What you can achieve by the end of this guide:
Tapping on a link like https://your-domain.com/details would lead to opening a specific screen of your app.
If you are looking to use a custom scheme like myscheme123://details then this guide is not for you. You may check out the app_links package.
What you need to do:
Configure the routes in your Flutter project
Configure AndroidManifest.xml
Host assetlinks.json file on your web server (for http/https scheme)
Test it with adb commands (you can already validate your setup with adb commands, even without hosting the file)
What you need to know:
There are two ways to configure deep linking or app linking in Flutter: go_router or app_links (or uni_links), each of them has their specific behaviour and setups (see section below)
The fingerprints in the assetlinks.json file matter - this would determine whether tapping on your custom link will automatically open your app
Now let's get started.
go_router or app_links ? (or both)This depends on your use case. I like how easy it is to set up with go_router , but it comes with a huge limitation (which is often critical to me). So when you use go_router, you set up the GoRouter instance as usual, plus a one-line setting in AndroidManifest.xml and then the deep linking setup on the Flutter part is done - however, the underlying behaviour of the navigation with this approach is equivalent to context.go(<path>), which means you lose your navigation stack the moment you're navigated to the <path> you specified. Which is often a dealbreaker for me, ugh. (See this article for the difference between go and push, though I don't agree with the author saying we should avoid using push as much as possible, push is still useful for mobile-only apps, and some cases like dialogs)
For setup with app_links, it's a bit more complicated, see here for detailed setup. I would avoid uni_links and Firebase Dynamic Links by the way - uni_links has not been updated since two years ago, and Firebase Dynamic Links is officially deprecated. Here is my setup:
// At a top-level widget
class App extends StatefulWidget {
const App({super.key});
@override
State<App> createState() => _AppState();
}
class _AppState extends State<App> {
late final AppLinks _appLinks;
StreamSubscription<String>? _linkSubscription;
@override
void initState() {
_initDeepLinks();
super.initState();
}
@override
void dispose() {
_linkSubscription?.cancel();
super.dispose();
}
Future<void> _initDeepLinks() async {
_appLinks = AppLinks();
_linkSubscription = _appLinks.allStringLinkStream.listen((url) {
router.push(url); // with your GoRouter instance
});
}
@override
Widget build(BuildContext context) {
return MaterialApp.router(
routerConfig: router,
builder: ...
);
}
}
In my projects I usually use both go_router and app_links - because I like the declarative routing API in go_router, and app_links I need for achieving context.push(<my_deeplink_path>) . (I really wish go_router could let us customise this behaviour so I can get rid of the app_links dependency...)
AndroidManifest.xmlThis is the key component to configure for Android deep links to work. Here is what the official Flutter guide ask you to do:
<meta-data android:name="flutter_deeplinking_enabled" android:value="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" />
</intent-filter>
Put this under the main <Activity> (you should only have one Activity for your Flutter app anyway), replace the android:host value accordingly, and you're good to go. Note that if you are relying on app_links to do the navigation you should remove the line with the flutter_deeplinking_enabled . The autoVerify=true is to make sure that Android will automatically try to verify your app with the host you specified, so that the app association with the host is established. Users can still manually override this, of course. You can later check that behaviour in your Android device / emulator when you have the app installed after this is configured (select your app, go to "App Info", then go to "Open by default"):

Only after completing all the steps in this guide you would see your domain being specified in the "verified links". You can manually add link, of course, but you cannot expect your users to do that. So be patient and follow through the rest of the setup.
Side note: The default android:launchMode of Flutter activity is singleTop , however, as pointed out here, opening the app link would end up triggering another instance of your app, which is sub-optimal. Setting the launch mode to singleInstance is better, plus avoiding task hijacking altogether when you also set android:taskAffinity="" in the <application> tag.
assetlinks.json and host it on your web serverSteps listed here is good enough, but there are a few things worth mentioning:
~/.android/debug.keystore in Mac systems) into the assetlinks.json file. To get the signature of your keystore, run this in your terminal: keytool -list -v -keystore <path to your keystore> .You can use a single assetlinks.json file to support multiple app bundles, e.g. if you have multiple app flavours, you can do:
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp.dev",
"sha256_cert_fingerprints": [
"<fingerprint_one>",
"<fingerprint_two>"
]
}
},
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp.qa",
"sha256_cert_fingerprints": [
"<fingerprint_one>",
"<fingerprint_two>"
]
}
},
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints": [
"<fingerprint_one>",
"<fingerprint_two>"
]
}
}
]
Allow some time after reinstalling the app for Google to pick up the association. Google suggests waiting at least 20 seconds for the asynchronous verification to complete.
Tips if you don't have a web server: You can use Firebase Hosting and upload the assetlinks.json as a static file (make sure the final path is .well-known/assetlinks.json and the domain is what you created in Firebase Hosting).
Now if you have done everything right, you can type a URL in a separate app (e.g. in a Note app, or send an email to yourself, or type in a Google Doc) and then tap on the URL (note: typing or pasting the URL in the browser would not work!), you will see the auto-navigation to the specific screen of your app. But, if something is not right, you can debug and test with adb commands:
adb shell 'am start -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "https://dev.example.com/product-details"' com.example.myapp.dev to test if it opens the correct screen of your app (would still work even if the host association is not done)
adb shell dumpsys package com.example.myapp.dev to check everything about this package including the host association and auto-verification status
And that is it! I will write about the setup for iOS in the next part.