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.
In Android, there are two types of Uni Links:
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 --> <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
).
In iOS, there are also two types of uni links:
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>
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.
As mentioned earlier, there are two ways that your app will handle a deep link:
_initURIHandler
will be invoked and have the initial linkThe _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:
Uri
variables to identify the initial and active/current URI,Object
to store the error in case of link parsing malfunctionsStreamSubscription
object to listen to incoming links when the app is in the foregroundNext, 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:
_initURIHandler
will only be called once even in case of a widget being disposed offluttertoast
package when this method was invokedgetInitialUri
method to parse and return the link as a new URI in initialURI
variableinitialURI
is null or not. If not null, set up the _initialURI
value w.r.t initialURI
PlatformException
FormatException
if the link is not valid as a URINext, 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:
_currentURI
and _err
variablesonError
event and updated the _currentURI
and _err
variablesAfter 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:
_initialURI
variable_currentURI
variableBuild and restart your application; it’ll look like this:
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.
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://unilinks.@@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.
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://unilinks.@@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
.
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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
5 Replies to "Understanding deep linking in Flutter with Uni Links"
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.
Hi, I’m sorry if I’m missing something, I’m fairly new to both flutter and uni-links. Where did you set variable `mounted` and what’s the code inside function `getInitialUri` looks like? can you give me a link to the source code?
Thanks
Hey Riki, here is the link for the source code: https://github.com/himanshusharma89/uni_links_example
Also,
– We use mounted to check whether the widget has state so that it can be updated.
– You can find the getInitialUri within the uni_links package
How can redirect the user to playstore or App Store if the app is not installed, considering the fact that Firebase Dynamic link service will be discontinued soon.
Any way around this?
I am having the opposite problem: Rather than incoming links, I am trying to launch an external app using its universal link. My flutter app is a web app. In testing mode the link (to Uber in this case) works as expected, but when I publish the app the link does not work (on iOS). Any advice?