# The missing guide to deep linking in Flutter apps - Part 1, Android

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`](https://pub.dev/packages/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`](https://pub.dev/packages/go_router) or [`app_links`](https://pub.dev/packages/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](https://docs.flutter.dev/cookbook/navigation/set-up-app-links#1-customize-a-flutter-application), plus [a one-line setting in `AndroidManifest.xml`](https://docs.flutter.dev/cookbook/navigation/set-up-app-links#2-modify-androidmanifestxml) 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](https://github.com/flutter/flutter/issues/134373). Which is often a dealbreaker for me, ugh. (See [this article](https://codewithandrea.com/articles/flutter-navigation-gorouter-go-vs-push/) 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](https://pub.dev/packages/app_links) 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](https://firebase.google.com/support/dynamic-links-faq). Here is my setup:

```dart
// 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...)

# Configure `AndroidManifest.xml`

This is the key component to configure for Android deep links to work. Here is what the [official Flutter guide](https://docs.flutter.dev/cookbook/navigation/set-up-app-links) ask you to do:

```xml
 <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"):

![Select your app > App info > Open by default](https://cdn.hashnode.com/res/hashnode/image/upload/v1709199132022/6b212321-cd51-420e-b03c-e7cd17ae76a8.png align="center")

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](https://pub.dev/packages/app_links#getting-started), 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](https://blog.dixitaditya.com/android-task-hijacking?x-host=blog.dixitaditya.com) altogether when you also set `android:taskAffinity=""` in the `<application>` tag.

# Create `assetlinks.json` and host it on your web server

[Steps listed here](https://docs.flutter.dev/cookbook/navigation/set-up-app-links#3-hosting-assetlinksjson-file) is good enough, but there are a few things worth mentioning:

* The SHA256 fingerprints from the Google Play Developer Console is good enough for builds that are released via Google Play. This is due to the fact that the fingerprints are generated from the keystore used to sign the APK - you might be relying on Google Play's own provided keystore, or you may have uploaded your own. Either way, if you also want to test locally in the debug mode, you need to also add the fingerprint of your debug keystore (often located in `~/.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:

```xml
[
  {
    "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](https://developer.android.com/training/app-links/verify-android-applinks#auto-verification) for the asynchronous verification to complete.
    
* Tips if you don't have a web server: You can use [Firebase Hosting](https://firebase.google.com/docs/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).
    

# Test with ADB commands

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.
