uni_links 0.1.4

  • README.md
  • CHANGELOG.md
  • Example
  • Installing
  • Versions
  • 95

Uni Links

Travis' Continuous Integration build status

A Flutter plugin project to help with App/Deep Links (Android) and Universal Links and Custom URL schemes (iOS).

These links are simply web-browser-like-links that activate your app and may contain information that you can use to load specific section of the app or continue certain user activity from a website (or another app).

App Links and Universal Links are regular https links, thus if the app is not installed (or setup correctly) they'll load in the browser, allowing you to present a web-page for further action, eg. install the app.

Installation

To use the plugin, add uni_links as a dependency in your pubspec.yaml file.

Permission

Android and iOS require to declare links' permission in a configuration file.

Feel free to examine tha example app in the example directory for Deep Links (Android) and Custom URL schemes (iOS).

For Android

Uni Links supports two types of Android links: "App Links" and "Deep Links".

  • App Links only work with https scheme and require a specified host, plus a hosted file - assetlinks.json. Check the Guide links below.
  • Deep Links can have any custom scheme and do not require a host, nor a hosted file. The downside is that any app can claim a scheme + host combo, so make sure yours are as unique as possible, eg. HST0000001://host.com.

You need to declare at least one of the two permissions in android/app/src/main/AndroidManifest.xml:

<manifest ...>
  <!-- ... other tags -->
  <application ...>
    <activity ...>
      <!-- ... other tags -->

      <!-- 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" />
        <data
          android:scheme="[YOUR_SCHEME]"
          android:host="[YOUR_HOST]" />
      </intent-filter>

      <!-- 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" />
        <data
          android:scheme="https"
          android:host="[YOUR_HOST]" />
      </intent-filter>
    </activity>
  </application>
</manifest>

The android:host attribute is optional for Deep Links.

To further the specificity you can add an android:pathPrefix attribute:

<data
  android:scheme="[YOUR_SCHEME_OR_HTTPS]"
  android:host="[YOUR_HOST]"
  android:pathPrefix="/[NAME][/NAME...]" />

For more info read The Ultimate Guide. Pay close attention to the App Links section in the Guide regarding the required /.well-known/assetlinks.json file.

The Android developer docs are also a great source of information for both Deep Links and App Links.

For iOS

There are two kinds of links in iOS: "Universal Links" and "Custom URL schemes".

  • Universal Links only work with https scheme and require a specified host, entitlements and a hosted file - apple-app-site-association. Check the Guide links below.
  • Custom URL schemes can have... any custom scheme and there is no host specificity, nor entitlements or a hosted file. The downside is that any app can claim any scheme, so make sure yours is as unique as possible, eg. hst0000001 or myIncrediblyAwesomeScheme.

You need to declare at least one of the two.

--

For Universal Links you need to add or create a com.apple.developer.associated-domains entitlement - either through Xcode or by editing (or creating and adding to Xcode) 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>

At the moment Flutter v0.4.4 does not support Universal Links, so a bit of Objective-C code will be required in ios/Runner/AppDelegate.m:

// ... other imports
#import <uni_links/UniLinksPlugin.h>

@implementation AppDelegate

// ... other methods

// NOTE: Necessary, until Flutter supports
//       `application:continueUserActivity:restorationHandler` within the
//       `FlutterPlugin` protocol.
- (BOOL)application:(UIApplication *)application continueUserActivity:(NSUserActivity *)userActivity restorationHandler:(void (^)(NSArray * _Nullable))restorationHandler {
    return [[UniLinksPlugin sharedInstance] application:application continueUserActivity:userActivity restorationHandler:restorationHandler];
}

@end

Alternatively for Swift enabled apps edit your bridging header file in ios/Runner/Runner-Bridging-Header.h:

#import "GeneratedPluginRegistrant.h"
// ... other imports
#import <uni_links/UniLinksPlugin.h>

Then edit the AppDelegate in ios/Runner/AppDelegate.swift:

// ... imports

@UIApplicationMain
@objc class AppDelegate: FlutterAppDelegate {
    // ... other methods

  override func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([Any]?) -> Void) -> Bool {
    return UniLinksPlugin.sharedInstance().application(application, continue: userActivity, restorationHandler: restorationHandler)
  }
}

NOTE: If you get errors, make sure to run flutter run so the pod installation can occur.

For more information, read Apple's guide for Universal Links.

--

For Custom URL schemes you need to declare the scheme in ios/Runner/Info.plist (or through Xcode's Target Info editor, under URL Types):

<?xml ...>
<!-- ... other tags -->
<plist>
<dict>
  <!-- ... other tags -->
  <key>CFBundleURLTypes</key>
  <array>
    <dict>
      <key>CFBundleTypeRole</key>
      <string>Editor</string>
      <key>CFBundleURLName</key>
      <string>[ANY_URL_NAME]</string>
      <key>CFBundleURLSchemes</key>
      <array>
        <string>[YOUR_SCHEME]</string>
      </array>
    </dict>
  </array>
  <!-- ... other tags -->
</dict>
</plist>

For a little more information, read Apple's guide for Inter-App Communication.

I strongly recommend watching the Apple WWDC 2015, session 509 - Seamless Linking to Your App to understand how the Universal Links work (and are setup).

Usage

Returns the link that the app was started with, if any.

import 'dart:async';
import 'dart:io';

import 'package:uni_links/uni_links.dart';
import 'package:flutter/services.dart' show PlatformException;

// ...

  Future<Null> initUniLinks() async {
    // Platform messages may fail, so we use a try/catch PlatformException.
    try {
      String initialLink = await getInitialLink();
      // Parse the link and warn the user, if it is not correct,
      // but keep in mind it could be `null`.
    } on PlatformException {
      // Handle exception by warning the user their action did not succeed
      // return?
    }
  }

// ...

Same as the getInitialLink, but converted to a Uri.

    // Uri parsing may fail, so we use a try/catch FormatException.
    try {
      Uri initialUri = await getInitialUri();
      // Use the uri and warn the user, if it is not correct,
      // but keep in mind it could be `null`.
    } on FormatException {
      // Handle exception by warning the user their action did not succeed
      // return?
    }
    // ... other exception handling like PlatformException

One can achieve the same by using Uri.parse(initialLink), which is what this convenience method does.

On change event (String)

Usually you would check the getInitialLink and also listen for changes.

import 'dart:async';
import 'dart:io';

import 'package:uni_links/uni_links.dart';

// ...

  StreamSubscription _sub;

  Future<Null> initUniLinks() async {
    // ... check initialLink

    // Attach a listener to the stream
    _sub = getLinksStream().listen((String link) {
      // Parse the link and warn the user, if it is not correct
    }, onError: (err) {
      // Handle exception by warning the user their action did not succeed
    });

    // NOTE: Don't forget to call _sub.cancel() in dispose()
  }

// ...

On change event (Uri)

Same as the stream, but transformed to emit Uri objects.

Usually you would check the getInitialUri and also listen for changes.

import 'dart:async';
import 'dart:io';

import 'package:uni_links/uni_links.dart';

// ...

  StreamSubscription _sub;

  Future<Null> initUniLinks() async {
    // ... check initialUri

    // Attach a listener to the stream
    _sub = getUriLinksStream().listen((Uri uri) {
      // Use the uri and warn the user, if it is not correct
    }, onError: (err) {
      // Handle exception by warning the user their action did not succeed
    });

    // NOTE: Don't forget to call _sub.cancel() in dispose()
  }

// ...

If you register a schema, say unilink, you could use these cli tools:

Android

You could do below tasks within Android Studio.

Assuming you've installed Android Studio (with the SDK platform tools):

adb shell 'am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "unilinks://host/path/subpath"'
adb shell 'am start -W -a android.intent.action.VIEW -c android.intent.category.BROWSABLE -d "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 "unilinks://example.com/?arr%5b%5d=123&arr%5b%5d=abc&addr=1%20Nowhere%20Rd&addr=Rand%20City%F0%9F%98%82"'

If you don't have adb in your path, but have $ANDROID_HOME env variable then use "$ANDROID_HOME"/platform-tools/adb ....

Note: Alternatively you could simply enter an adb shell and run the am commands in it.

Note: I use single quotes, because what follows the shell command is what will run in the emulator (or device) and shell metacharacters, such as question marks (?) and ampersands (&), usually mean something different to your own shell.

adb shell communicates with the only available device (or emulator), so if you've got multiple devices you have to specify which one you want to run the shell in via:

  • The only USB connected device - adb -d shell '...'
  • The only emulated device - adb -e shell '...'

You could use adb devices to list currently available devices (similarly flutter devices does the same job).

iOS

Assuming you've got Xcode already installed:

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

If you've got xcrun (or simctl) in your path, you could invoke it directly.

The flag booted assumes an open simulator (you can start it via open -a Simulator) with a booted device. You could target specific device by specifying its UUID (found via xcrun simctl list or flutter devices), replacing the booted flag.

These types of links use https for schema, thus you can use above examples by replacing unilinks with https.

Contributing

For help on editing plugin code, view the documentation.

License

BSD 2-clause

[0.1.4] - 2018-10-16

  • [Android] Don't process links when launched in background. (@wkornewald)

[0.1.3] - 2018-09-08

  • No code changes.
  • Added section in the README about Swift-enabled apps.
  • Added section in the README about tooling for invoking links from the cli.

[0.1.2] - 2018-07-30

  • Fixed lost initialLink on iOS launch via Universal Link. (@wkornewald)

[0.1.1] - 2018-05-31

  • No code changes. Pushed to pub.dartlang.org with an untracked file that had to be manually removed.

[0.1.0] - 2018-05-31

  • Initial release.

example/lib/main.dart

import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:uni_links/uni_links.dart';

void main() => runApp(new MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => new _MyAppState();
}

enum UniLinksType { string, uri }

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  String _latestLink = 'Unknown';
  Uri _latestUri;

  StreamSubscription _sub;

  TabController _tabController;
  UniLinksType _type = UniLinksType.string;

  final List<String> _cmds = getCmds();
  final TextStyle _cmdStyle = const TextStyle(
      fontFamily: 'Courier', fontSize: 12.0, fontWeight: FontWeight.w700);
  final _scaffoldKey = new GlobalKey<ScaffoldState>();

  @override
  initState() {
    super.initState();
    _tabController = new TabController(vsync: this, length: 2);
    _tabController.addListener(_handleTabChange);
    initPlatformState();
  }

  @override
  dispose() {
    if (_sub != null) _sub.cancel();
    _tabController.removeListener(_handleTabChange);
    _tabController.dispose();
    super.dispose();
  }

  // Platform messages are asynchronous, so we initialize in an async method.
  initPlatformState() async {
    if (_type == UniLinksType.string) {
      await initPlatformStateForStringUniLinks();
    } else {
      await initPlatformStateForUriUniLinks();
    }
  }

  /// An implementation using a [String] link
  initPlatformStateForStringUniLinks() async {
    // Attach a listener to the links stream
    _sub = getLinksStream().listen((String link) {
      if (!mounted) return;
      setState(() {
        _latestLink = link ?? 'Unknown';
        _latestUri = null;
        try {
          if (link != null) _latestUri = Uri.parse(link);
        } on FormatException {}
      });
    }, onError: (err) {
      if (!mounted) return;
      setState(() {
        _latestLink = 'Failed to get latest link: $err.';
        _latestUri = null;
      });
    });

    // Attach a second listener to the stream
    getLinksStream().listen((String link) {
      print('got link: $link');
    }, onError: (err) {
      print('got err: $err');
    });

    // Get the latest link
    String initialLink;
    Uri initialUri;
    // Platform messages may fail, so we use a try/catch PlatformException.
    try {
      initialLink = await getInitialLink();
      print('initial link: $initialLink');
      if (initialLink != null) initialUri = Uri.parse(initialLink);
    } on PlatformException {
      initialLink = 'Failed to get initial link.';
      initialUri = null;
    } on FormatException {
      initialLink = 'Failed to parse the initial link as Uri.';
      initialUri = null;
    }

    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;

    setState(() {
      _latestLink = initialLink;
      _latestUri = initialUri;
    });
  }

  /// An implementation using the [Uri] convenience helpers
  initPlatformStateForUriUniLinks() async {
    // Attach a listener to the Uri links stream
    _sub = getUriLinksStream().listen((Uri uri) {
      if (!mounted) return;
      setState(() {
        _latestUri = uri;
        _latestLink = uri?.toString() ?? 'Unknown';
      });
    }, onError: (err) {
      if (!mounted) return;
      setState(() {
        _latestUri = null;
        _latestLink = 'Failed to get latest link: $err.';
      });
    });

    // Attach a second listener to the stream
    getUriLinksStream().listen((Uri uri) {
      print('got uri: ${uri?.path} ${uri?.queryParametersAll}');
    }, onError: (err) {
      print('got err: $err');
    });

    // Get the latest Uri
    Uri initialUri;
    String initialLink;
    // Platform messages may fail, so we use a try/catch PlatformException.
    try {
      initialUri = await getInitialUri();
      print('initial uri: ${initialUri?.path}'
          ' ${initialUri?.queryParametersAll}');
      initialLink = initialUri?.toString();
    } on PlatformException {
      initialUri = null;
      initialLink = 'Failed to get initial uri.';
    } on FormatException {
      initialUri = null;
      initialLink = 'Bad parse the initial link as Uri.';
    }

    // If the widget was removed from the tree while the asynchronous platform
    // message was in flight, we want to discard the reply rather than calling
    // setState to update our non-existent appearance.
    if (!mounted) return;

    setState(() {
      _latestUri = initialUri;
      _latestLink = initialLink;
    });
  }

  @override
  Widget build(BuildContext context) {
    final queryParams = _latestUri?.queryParametersAll?.entries?.toList();

    return new MaterialApp(
      home: new Scaffold(
        key: _scaffoldKey,
        appBar: new AppBar(
          title: new Text('Plugin example app'),
          bottom: new TabBar(
            controller: _tabController,
            tabs: <Widget>[
              new Tab(text: 'STRING LINK'),
              new Tab(text: 'URI'),
            ],
          ),
        ),
        body: new ListView(
          shrinkWrap: true,
          padding: const EdgeInsets.all(8.0),
          children: <Widget>[
            new ListTile(
              title: const Text('Link'),
              subtitle: new Text('$_latestLink'),
            ),
            new ListTile(
              title: const Text('Uri Path'),
              subtitle: new Text('${_latestUri?.path}'),
            ),
            new ExpansionTile(
              initiallyExpanded: true,
              title: const Text('Query params'),
              children: queryParams?.map((item) {
                    return new ListTile(
                      title: new Text('${item.key}'),
                      trailing: new Text('${item.value?.join(', ')}'),
                    );
                  })?.toList() ??
                  <Widget>[
                    new ListTile(
                      dense: true,
                      title: const Text('null'),
                    ),
                  ],
            ),
            _cmdsCard(_cmds),
          ],
        ),
      ),
    );
  }

  Widget _cmdsCard(commands) {
    Widget platformCmds;

    if (commands == null) {
      platformCmds = const Center(child: const Text('Unsupported platform'));
    } else {
      platformCmds = new Column(
        children: <List<Widget>>[
          [
            const Text(
                'To populate above fields open a terminal shell and run:\n')
          ],
          intersperse(
              commands.map<Widget>((cmd) => new InkWell(
                    onTap: () => _printAndCopy(cmd),
                    child: new Text('\n$cmd\n', style: _cmdStyle),
                  )),
              const Text('or')),
          [
            new Text(
                '(tap on any of the above commands to print it to'
                ' the console/logger and copy to the device clipboard.)',
                textAlign: TextAlign.center,
                style: Theme.of(context).textTheme.caption),
          ]
        ].expand((el) => el).toList(),
      );
    }

    return new Card(
      margin: const EdgeInsets.only(top: 20.0),
      child: new Padding(
        padding: const EdgeInsets.all(10.0),
        child: platformCmds,
      ),
    );
  }

  _handleTabChange() {
    if (_tabController.indexIsChanging) {
      setState(() {
        _type = UniLinksType.values[_tabController.index];
      });
      initPlatformState();
    }
  }

  _printAndCopy(String cmd) async {
    print(cmd);

    await Clipboard.setData(new ClipboardData(text: cmd));
    _scaffoldKey.currentState.showSnackBar(new SnackBar(
      content: const Text('Copied to Clipboard'),
    ));
  }
}

List<String> getCmds() {
  String cmd;
  String cmdSuffix = '';

  if (Platform.isIOS) {
    cmd = '/usr/bin/xcrun simctl openurl booted';
  } else if (Platform.isAndroid) {
    cmd = '\$ANDROID_HOME/platform-tools/adb shell \'am start'
        ' -a android.intent.action.VIEW'
        ' -c android.intent.category.BROWSABLE -d';
    cmdSuffix = "'";
  } else {
    return null;
  }

  // https://orchid-forgery.glitch.me/mobile/redirect/
  return [
    '$cmd "unilinks://host/path/subpath"$cmdSuffix',
    '$cmd "unilinks://example.com/path/portion/?uid=123&token=abc"$cmdSuffix',
    '$cmd "unilinks://example.com/?arr%5b%5d=123&arr%5b%5d=abc'
        '&addr=1%20Nowhere%20Rd&addr=Rand%20City%F0%9F%98%82"$cmdSuffix',
  ];
}

List<Widget> intersperse(Iterable<Widget> list, Widget item) {
  List<Widget> initialValue = [];
  return list.fold(initialValue, (all, el) {
    if (all.length != 0) all.add(item);
    all.add(el);
    return all;
  });
}

Use this package as a library

1. Depend on it

Add this to your package's pubspec.yaml file:


dependencies:
  uni_links: ^0.1.4

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter packages get

Alternatively, your editor might support flutter packages get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:uni_links/uni_links.dart';
  
Version Uploaded Documentation Archive
0.1.4 Oct 16, 2018 Go to the documentation of uni_links 0.1.4 Download uni_links 0.1.4 archive
0.1.3 Sep 8, 2018 Go to the documentation of uni_links 0.1.3 Download uni_links 0.1.3 archive
0.1.2 Jul 29, 2018 Go to the documentation of uni_links 0.1.2 Download uni_links 0.1.2 archive
0.1.1 May 31, 2018 Go to the documentation of uni_links 0.1.1 Download uni_links 0.1.1 archive
0.1.0 May 31, 2018 Go to the documentation of uni_links 0.1.0 Download uni_links 0.1.0 archive
Popularity:
Describes how popular the package is relative to other packages. [more]
90
Health:
Code health derived from static analysis. [more]
100
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
100
Overall:
Weighted score of the above. [more]
95
Learn more about scoring.

We analyzed this package on Oct 16, 2018, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.0.0
  • pana: 0.12.4
  • Flutter: 0.9.5

Platforms

Detected platforms: Flutter

References Flutter, and has no conflicting libraries.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.0.0-dev.28.0 <3.0.0
flutter 0.0.0
Transitive dependencies
collection 1.14.11
meta 1.1.6
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8
Dev dependencies
flutter_test