Himanshu Sharma Computer science student pursuing his Bachelor's and working as an SWE Intern. For the past 2 years, has been developing mobile and web apps using Flutter SDK. Open-source enthusiast and co-organizer of Flutter India.

Understanding deep linking in Flutter with Uni Links

7 min read 1985

Understanding Deep Linking In Flutter With uni_links

Deep linking provides you with a web browser link that points to a specific part of an app that is already installed. These links can also be set to navigate users to specific content pages (like events, news updates, and more) and pass through custom data (like promo codes).

For example, if you want to share this article with a friend, then you would send a URL pointing to this article, not the blog.logrocket.com website to navigate through and look for the article themselves. This means you need to handle the way your application is triggered manually or using a deep link.

Additionally, your app might already be running when the deep link is triggered, so you need to handle deep link clicks in your background running app as well.

In this tutorial, you’ll learn how to use uni_links to help you with this.

N.B., if you are new to Flutter, please go through the official documentation to learn about it.

Uni Links (uni_links) is a Flutter plugin used for receiving incoming App/Deep Links (for Android) and universal links and custom URL schemes (for iOS).

It currently supports Android, iOS, and web platforms.

Add uni_links in the pubspec dependencies:

uni_links: ^0.5.1

Next, you need to declare the link’s schema in the Android or iOS configuration file.

Android config

In Android, there are two types of Uni Links:



  • App Links: This link requires a specified host, a hosted file (assetlinks.json), and it only works with the https scheme (https://your_host). Here’s the App Link intent filter that you need to add to your configuration file. You can change the host to your liking:
    <!-- App Links -->
          <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" />
            <!-- Accepts URIs that begin with https://YOUR_HOST -->
            <data
              android:scheme="https"
              android:host="unilinks.example.com" />
          </intent-filter>
  • Deep Links: This link doesn’t require a host, a hoster file, or any custom scheme. It provides a way to utilize your app using URL: your_scheme://any_host. Here’s the Deep Link intent filter that you need to add to your configuration. You can also change the scheme and host:
    <!-- Deep Links --> <intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" /> 
    <category android:name="android.intent.category.BROWSABLE" /> 
    <!-- Accepts URIs that begin with YOUR_SCHEME://YOUR_HOST --> 
    <data android:scheme="logrckt" android:host="unilinks.example.com" /> 
    </intent-filter>

You need to declare either of these intent filters in your main AndroidManifest.xml file (android/app/src/main/AndroidManifest.xml).

iOS config

In iOS, there are also two types of uni links:

  • Universal Links: These only work with the https scheme and require a specified host, entitlements, and a hosted file. Similar to App Links in Android. You need to add or create a com.apple.developer.associated-domains entitlement, either through Xcode or by editing the ios/Runner/Runner.entitlements file:
    <?xml version="1.0" encoding="UTF-8"?>
    <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
    <plist version="1.0">
    <dict>
      <!-- ... other keys -->
      <key>com.apple.developer.associated-domains</key>
      <array>
        <string>applinks:[YOUR_HOST]</string>
      </array>
      <!-- ... other keys -->
    </dict>
    </plist>
  • Custom URL: This URL doesn’t require a host, entitlements, a hosted file, or any custom scheme. Similar to a Deep Link in Android, you need to add the host and scheme in the ios/Runner/Info.plist file as below:
    <key>CFBundleURLTypes</key>
      <array>
          <dict>
              <key>CFBundleTypeRole</key>
              <string>Editor</string>
              <key>CFBundleURLName</key>
              <string>unilinks.example.com</string>
              <key>CFBundleURLSchemes</key>
              <array>
                  <string>logrckt</string>
              </array>
          </dict>
      </array>

N.B, any app can claim your scheme and host combination in the case of Deep Links in Android and Custom URLs in iOS, so ensure that your host and scheme are as unique as possible.

Usage

As mentioned earlier, there are two ways that your app will handle a deep link:

  • Cold start: A cold start is starting the app anew if the app was terminated (not running in the background). In this case, _initURIHandler will be invoked and have the initial link
  • Coming back to the foreground: If the app is running in the background and you need to bring it back to the foreground, the Stream will produce the link. The initial link can either be null or be the link with which the app started

The _initURIHandler should be handled only once in your app’s lifecycle, as it is used to start the app and not to change throughout the app journey. So, create a global variable _initialURILinkHandled as false anywhere in your main.dart:

bool _initialURILinkHandled = false;

In your main.dart file, clean your MyHomePage widget by removing the existing code and creating new variables as below:

Uri? _initialURI;
Uri? _currentURI;
Object? _err;

StreamSubscription? _streamSubscription;

Here you are declaring:


More great articles from LogRocket:


  • Two Uri variables to identify the initial and active/current URI,
  • An Object to store the error in case of link parsing malfunctions
  • A StreamSubscription object to listen to incoming links when the app is in the foreground

Next, create the _initURIHandler method as below:

Future<void> _initURIHandler() async {
 // 1
 if (!_initialURILinkHandled) {
   _initialURILinkHandled = true;
   // 2
   Fluttertoast.showToast(
       msg: "Invoked _initURIHandler",
       toastLength: Toast.LENGTH_SHORT,
       gravity: ToastGravity.BOTTOM,
       timeInSecForIosWeb: 1,
       backgroundColor: Colors.green,
       textColor: Colors.white
   );
   try {
     // 3
     final initialURI = await getInitialUri();
     // 4
     if (initialURI != null) {
       debugPrint("Initial URI received $initialURI");
       if (!mounted) {
         return;
       }
       setState(() {
         _initialURI = initialURI;
       });
     } else {
       debugPrint("Null Initial URI received");
     }
   } on PlatformException { // 5
     debugPrint("Failed to receive initial uri");
   } on FormatException catch (err) { // 6
     if (!mounted) {
       return;
     }
     debugPrint('Malformed Initial URI received');
     setState(() => _err = err);
   }
 }
}

In the above code, you have done the following:

  1. Used a check here so that the _initURIHandler will only be called once even in case of a widget being disposed of
  2. Displayed a toast using the fluttertoast package when this method was invoked
  3. Used the getInitialUri method to parse and return the link as a new URI in initialURI variable
  4. Checked whether the initialURI is null or not. If not null, set up the _initialURI value w.r.t initialURI
  5. Handled the platform messages fail using PlatformException
  6. Handled the FormatException if the link is not valid as a URI

Next, create the _incomingLinkHandler method used to receive links while the app is already started:

void _incomingLinkHandler() {
 // 1
 if (!kIsWeb) {
   // 2
   _streamSubscription = uriLinkStream.listen((Uri? uri) {
     if (!mounted) {
       return;
     }
     debugPrint('Received URI: $uri');
     setState(() {
       _currentURI = uri;
       _err = null;
     });
     // 3
   }, onError: (Object err) {
     if (!mounted) {
       return;
     }
     debugPrint('Error occurred: $err');
     setState(() {
       _currentURI = null;
       if (err is FormatException) {
         _err = err;
       } else {
         _err = null;
       }
     });
   });
 }
}

This code did the following:

  1. Added a condition to check the platform, as the web platform will handle the link as an initial link only
  2. Listen to the stream of incoming links and update the _currentURI and _err variables
  3. Handled errors using the onError event and updated the _currentURI and _err variables

After creating these methods to listen to the incoming links, you need to call them before the Widget tree is rendered. Call these methods in the initState of the MyHomePage widget:

@override
void initState() {
 super.initState();
 _initURIHandler();
 _incomingLinkHandler();
}

Similarly, to let go of the resources when the app is terminated, close the StreamSubscription object in the dispose method:

@override
void dispose() {
 _streamSubscription?.cancel();
 super.dispose();
}

Next, update the build method:

@override
Widget build(BuildContext context) {
 return Scaffold(
     appBar: AppBar(
       title: Text(widget.title),
     ),
     body: Center(
         child: Padding(
       padding: const EdgeInsets.symmetric(horizontal: 20),
       child: Column(
         mainAxisAlignment: MainAxisAlignment.center,
         children: <Widget>[
           // 1
           ListTile(
             title: const Text("Initial Link"),
             subtitle: Text(_initialURI.toString()),
           ),
           // 2
           if (!kIsWeb) ...[
             // 3
             ListTile(
               title: const Text("Current Link Host"),
               subtitle: Text('${_currentURI?.host}'),
             ),
             // 4
             ListTile(
               title: const Text("Current Link Scheme"),
               subtitle: Text('${_currentURI?.scheme}'),
             ),
             // 5
             ListTile(
               title: const Text("Current Link"),
               subtitle: Text(_currentURI.toString()),
             ),
             // 6
             ListTile(
               title: const Text("Current Link Path"),
               subtitle: Text('${_currentURI?.path}'),
             )
           ],
           // 7
           if (_err != null)
             ListTile(
               title:
                   const Text('Error', style: TextStyle(color: Colors.red)),
               subtitle: Text(_err.toString()),
             ),
           const SizedBox(height: 20,),
           const Text("Check the blog for testing instructions")
         ],
       ),
     )));
}

Here, you have done the following:

  1. Displayed the Initial Link if received using the _initialURI variable
  2. Added a check to display the incoming links only on mobile platforms
  3. Displayed the host of the incoming link. We have already defined the host earlier
  4. Similar to host, displayed the scheme of the incoming link configured earlier
  5. Displayed the current or active incoming link using the _currentURI variable
  6. Displayed the path coming along with the host and scheme
  7. Displayed the error if it is not null

Build and restart your application; it’ll look like this:

Uni Links Demo Desktop

Uni Links Demo Mobile

All of the subtitles of the ListTile will be null for mobile because the app restarted manually. In the next step, you’ll test your changes.

You can test your changes by using CLI tools for invoking the links with your registered scheme.

Android

If Android Studio (with the SDK platform-tools) is already installed, you could do the following:

adb shell 'am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "logrckt://host/path/subpath"'
adb shell 'am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "logrckt://unilinks.example.com/path/portion/?uid=123&token=abc"'
adb shell 'am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "logrckt://unilinks.example.com/?arr%5b%5d=123&arr%5b%5d=abc&addr=1%20Nowhere%20Rd&addr=Rand%20City%F0%9F%98%82"'
adb shell 'am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "logrckt://[email protected]@malformed.invalid.url/path?"'

This will send calls according to your scheme and host to your app and your app will come to the foreground.

If you don’t have ADB in your path, but have the $ANDROID_HOME env variable, then use "$ANDROID_HOME"/platform-tools/… and the above commands.

Alternatively, you could simply run the command in an ADB shell.

iOS

If Xcode is already installed, you could do the following:

/usr/bin/xcrun simctl openurl booted "logrckt://host/path/subpath"
/usr/bin/xcrun simctl openurl booted "logrckt://unilinks.example.com/path/portion/?uid=123&token=abc"
/usr/bin/xcrun simctl openurl booted "logrckt://unilinks.example.com/?arr%5b%5d=123&arr%5b%5d=abc&addr=1%20Nowhere%20Rd&addr=Rand%20City%F0%9F%98%82"
/usr/bin/xcrun simctl openurl booted "logrckt://[email protected]@malformed.invalid.url/path?"

If you have xcrun (or simctl) in your path, you could invoke it directly. The flag booted assumes an open simulator with a booted device.

N.B, for App Links or Universal Links, you can try the above example with the https scheme for logrckt.

 

Uni Links Demo iOS

Conclusion

You can find the final project here.

In this tutorial, you learned about deep linking and how can you use it in your Flutter app. For the next step, you can try deep linking using Firebase Dynamic Links because they work even if the app is not installed by the user by redirecting them to the Play/App Store for installation.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Himanshu Sharma Computer science student pursuing his Bachelor's and working as an SWE Intern. For the past 2 years, has been developing mobile and web apps using Flutter SDK. Open-source enthusiast and co-organizer of Flutter India.

One Reply to “Understanding deep linking in Flutter with Uni Links”

  1. I would say also for custom urls and deeplinks the https scheme is required, above all for Android devices, since most apps (e.g. SMS app) do not allow received URIs to be clickable without a scheme of http or https.

Leave a Reply