Flutter统一链接处理插件uni_links5的使用

发布于 1周前 作者 vueper 来自 Flutter

Flutter统一链接处理插件uni_links5的使用

1. 插件简介

uni_links 是一个Flutter插件,用于处理应用程序链接(App Links)和通用链接(Universal Links)以及自定义URL方案(Custom URL schemes)。这些链接类似于网页浏览器中的链接,可以激活你的应用,并可能包含信息以加载应用的特定部分或从网站(或其他应用)继续某些用户活动。

2. 安装

要使用该插件,首先需要在 pubspec.yaml 文件中添加 uni_links 作为依赖项:

dependencies:
  uni_links: ^5.0.0
2.1 Android配置

Uni Links 支持两种类型的Android链接:App Links 和 Deep Links。

  • App Links 只支持 https 方案,并且需要指定主机以及托管文件 assetlinks.json
  • Deep Links 可以使用任何自定义方案,不需要主机或托管文件。但为了确保唯一性,建议使用唯一的方案+主机组合,例如 HST0000001://host.com

你需要在 android/app/src/main/AndroidManifest.xml 中声明至少一个Intent过滤器:

<manifest ...>
  <application ...>
    <activity ...>
      <!-- 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>

你可以通过添加 android:pathPrefix 属性来进一步指定路径:

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

iOS 支持两种类型的链接:通用链接(Universal Links)和自定义URL方案(Custom URL schemes)。

  • 通用链接 只支持 https 方案,并且需要指定主机、权限以及托管文件 apple-app-site-association
  • 自定义URL方案 可以使用任何自定义方案,没有主机限制或权限要求。但为了确保唯一性,建议使用唯一的方案,例如 hst0000001myIncrediblyAwesomeScheme

对于 通用链接,你需要在 ios/Runner/Runner.entitlements 文件中添加 com.apple.developer.associated-domains 权限:

<?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>
  <key>com.apple.developer.associated-domains</key>
  <array>
    <string>applinks:[YOUR_HOST]</string>
  </array>
</dict>
</plist>

你也可以通过Xcode的界面来创建这个文件:

  1. 打开Xcode,双击 ios/Runner.xcworkspace 文件。
  2. 在项目导航器中选择 Runner 根项。
  3. 选择 Runner target,然后选择 Signing & Capabilities 选项卡。
  4. 点击 + Capability 按钮,添加 Associated Domains
  5. 将第一个域从 webcredentials:example.com 更改为 applinks: + 你的主机(例如 my-fancy-domain.com)。

对于 自定义URL方案,你需要在 ios/Runner/Info.plist 文件中声明方案:

<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>

3. 使用方法

3.1 初始链接 (String)

getInitialLink 返回应用启动时的链接(如果有)。你应该在应用生命周期的早期处理这个链接,并且只处理一次。

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

Future<void> initUniLinks() async {
  try {
    final initialLink = await getInitialLink();
    // 解析链接并根据需要处理
  } on PlatformException {
    // 处理异常
  }
}
3.2 初始链接 (Uri)

getInitialUrigetInitialLink 类似,但返回的是 Uri 对象。

try {
  final initialUri = await getInitialUri();
  // 使用 Uri 并根据需要处理
} on FormatException {
  // 处理格式异常
} on PlatformException {
  // 处理平台异常
}
3.3 链接变化事件 (String)

通常你会检查 getInitialLink,同时监听链接的变化。

StreamSubscription? _sub;

Future<void> initUniLinks() async {
  // 检查初始链接

  // 监听链接变化
  _sub = linkStream.listen((String? link) {
    // 解析链接并根据需要处理
  }, onError: (err) {
    // 处理异常
  });

  // 注意:不要忘记在 dispose 中取消订阅
}
3.4 链接变化事件 (Uri)

uriLinkStreamlinkStream 类似,但返回的是 Uri 对象。

StreamSubscription? _sub;

Future<void> initUniLinks() async {
  // 检查初始 URI

  // 监听 URI 变化
  _sub = uriLinkStream.listen((Uri? uri) {
    // 使用 Uri 并根据需要处理
  }, onError: (err) {
    // 处理异常
  });

  // 注意:不要忘记在 dispose 中取消订阅
}
3.5 应用启动时的链接处理

如果应用是从终止状态启动的(即不在后台运行),getInitialLink 会返回启动应用的链接,而流不会产生链接。如果应用是在后台运行的,流会生成链接,而 getInitialLink 可能为 null 或是应用启动时的初始链接。

因此,你应该始终检查初始链接(或 URI),并订阅链接变化的流。

4. 完整示例代码

以下是一个完整的示例代码,展示了如何在Flutter应用中使用 uni_links 插件处理链接:

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

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

bool _initialUriIsHandled = false;

void main() => runApp(MaterialApp(home: MyApp()));

class MyApp extends StatefulWidget {
  [@override](/user/override)
  _MyAppState createState() => _MyAppState();
}

class _MyAppState extends State<MyApp> with SingleTickerProviderStateMixin {
  Uri? _initialUri;
  Uri? _latestUri;
  Object? _err;

  StreamSubscription? _sub;

  final _scaffoldKey = GlobalKey();
  final _cmds = getCmds();
  final _cmdStyle = const TextStyle(
      fontFamily: 'Courier', fontSize: 12.0, fontWeight: FontWeight.w700);

  [@override](/user/override)
  void initState() {
    super.initState();
    _handleIncomingLinks();
    _handleInitialUri();
  }

  [@override](/user/override)
  void dispose() {
    _sub?.cancel();
    super.dispose();
  }

  /// 处理传入的链接 - 应用程序已经启动时从操作系统接收的链接
  void _handleIncomingLinks() {
    if (!kIsWeb) {
      // 处理应用已经启动时的链接
      _sub = uriLinkStream.listen((Uri? uri) {
        if (!mounted) return;
        print('got uri: $uri');
        setState(() {
          _latestUri = uri;
          _err = null;
        });
      }, onError: (Object err) {
        if (!mounted) return;
        print('got err: $err');
        setState(() {
          _latestUri = null;
          if (err is FormatException) {
            _err = err;
          } else {
            _err = null;
          }
        });
      });
    }
  }

  /// 处理初始 Uri - 应用启动时的链接
  ///
  /// **注意**: `getInitialLink`/`getInitialUri` 应该在整个应用生命周期中只处理一次。
  Future<void> _handleInitialUri() async {
    if (!_initialUriIsHandled) {
      _initialUriIsHandled = true;
      _showSnackBar('_handleInitialUri called');
      try {
        final uri = await getInitialUri();
        if (uri == null) {
          print('no initial uri');
        } else {
          print('got initial uri: $uri');
        }
        if (!mounted) return;
        setState(() => _initialUri = uri);
      } on PlatformException {
        print('falied to get initial uri');
      } on FormatException catch (err) {
        if (!mounted) return;
        print('malformed initial uri');
        setState(() => _err = err);
      }
    }
  }

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

    return Scaffold(
      key: _scaffoldKey,
      appBar: AppBar(
        title: const Text('uni_links 示例应用'),
      ),
      body: ListView(
        shrinkWrap: true,
        padding: const EdgeInsets.all(8.0),
        children: [
          if (_err != null)
            ListTile(
              title: const Text('错误', style: TextStyle(color: Colors.red)),
              subtitle: Text('$_err'),
            ),
          ListTile(
            title: const Text('初始 Uri'),
            subtitle: Text('$_initialUri'),
          ),
          if (!kIsWeb) ...[
            ListTile(
              title: const Text('最新 Uri'),
              subtitle: Text('$_latestUri'),
            ),
            ListTile(
              title: const Text('最新 Uri (路径)'),
              subtitle: Text('${_latestUri?.path}'),
            ),
            ExpansionTile(
              initiallyExpanded: true,
              title: const Text('最新 Uri (查询参数)'),
              children: queryParams == null
                  ? const [ListTile(dense: true, title: Text('null'))]
                  : [
                      for (final item in queryParams)
                        ListTile(
                          title: Text(item.key),
                          trailing: Text(item.value.join(', ')),
                        )
                    ],
            ),
          ],
          _cmdsCard(_cmds),
          const Divider(),
          if (!kIsWeb)
            ListTile(
              leading: const Icon(Icons.error, color: Colors.red),
              title: const Text(
                '强制退出此示例应用',
                style: TextStyle(color: Colors.red),
              ),
              onTap: () {
                if (Platform.isIOS) {
                  exit(0);
                } else {
                  SystemNavigator.pop();
                }
              },
            ),
        ],
      ),
    );
  }

  Widget _cmdsCard(List<String>? commands) {
    Widget platformCmds;

    if (commands == null) {
      platformCmds = const Center(child: Text('不支持的平台'));
    } else {
      platformCmds = Column(
        children: [
          const [
            if (kIsWeb)
              Text('将此路径附加到 Web 应用的 URL,替换 `#/`:\n')
            else
              Text('要填充上述字段,请打开终端并运行:\n'),
          ],
          intersperse(
              commands.map<Widget>((cmd) => InkWell(
                    onTap: () => _printAndCopy(cmd),
                    child: Text('\n$cmd\n', style: _cmdStyle),
                  )),
              const Text('或')),
          [
            Text(
              '(点击上面的任何命令将其打印到控制台/日志并复制到设备剪贴板)',
              textAlign: TextAlign.center,
              style: Theme.of(context).textTheme.bodySmall,
            ),
          ]
        ].expand((el) => el).toList(),
      );
    }

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

  Future<void> _printAndCopy(String cmd) async {
    print(cmd);

    await Clipboard.setData(ClipboardData(text: cmd));
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('已复制到剪贴板')),
    );
  }

  void _showSnackBar(String msg) {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      final context = _scaffoldKey.currentContext;
      if (context != null) {
        ScaffoldMessenger.of(context).showSnackBar(SnackBar(
          content: Text(msg),
        ));
      }
    });
  }
}

List<String>? getCmds() {
  late final String cmd;
  var cmdSuffix = '';

  const plainPath = 'path/subpath';
  const args = 'path/portion/?uid=123&token=abc';
  const emojiArgs =
      '?arr%5b%5d=123&arr%5b%5d=abc&addr=1%20Nowhere%20Rd&addr=Rand%20City%F0%9F%98%82';

  if (kIsWeb) {
    return [
      plainPath,
      args,
      emojiArgs,
      // 无法创建无效 URL,因为浏览器会确保其有效性
    ];
  }

  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;
  }

  return [
    '$cmd "unilinks://host/$plainPath"$cmdSuffix',
    '$cmd "unilinks://example.com/$args"$cmdSuffix',
    '$cmd "unilinks://example.com/$emojiArgs"$cmdSuffix',
    '$cmd "unilinks://@@malformed.invalid.url/path?"$cmdSuffix',
  ];
}

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

更多关于Flutter统一链接处理插件uni_links5的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter统一链接处理插件uni_links5的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何在Flutter应用中使用uni_links插件来统一处理链接的示例代码。这个插件允许你的应用在启动时或前台运行时捕获和处理设备上的深度链接(deep links)。

首先,你需要在你的pubspec.yaml文件中添加uni_links依赖:

dependencies:
  flutter:
    sdk: flutter
  uni_links: ^0.5.1  # 请检查最新版本号

然后运行flutter pub get来获取依赖。

接下来,你需要在你的应用中进行以下设置:

  1. iOS配置

    • 在你的Info.plist文件中添加以下内容以允许应用响应自定义URL Scheme:
      <key>CFBundleURLTypes</key>
      <array>
        <dict>
          <key>CFBundleURLSchemes</key>
          <array>
            <string>yourappscheme</string> <!-- 替换为你的应用scheme -->
          </array>
        </dict>
      </array>
      
  2. Android配置

    • 在你的android/app/src/main/AndroidManifest.xml文件中添加以下内容来声明你的Intent Filter:
      <activity
        android:name=".MainActivity"
        android:launchMode="singleTop"
        android:theme="@style/LaunchTheme"
        android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
        android:hardwareAccelerated="true"
        android:windowSoftInputMode="adjustResize">
        <intent-filter>
          <action android:name="android.intent.action.MAIN" />
          <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
        <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="yourapp.com" /> <!-- 替换为你的应用链接 -->
          <data android:scheme="http" android:host="yourapp.com" /> <!-- 可选,如果你的应用也支持http -->
        </intent-filter>
      </activity>
      
  3. Flutter代码实现

    • 在你的main.dart文件中,添加以下代码来处理链接:

      import 'package:flutter/material.dart';
      import 'package:uni_links/uni_links.dart';
      
      void main() {
        runApp(MyApp());
      
        // 初始化uni_links监听器
        _initUniLinks();
      }
      
      void _initUniLinks() async {
        // 获取启动时的链接(如果有)
        final InitialUri? initialUri = await getInitialUri();
        handleDeepLink(initialUri);
      
        // 监听后续的链接
        uriLinkStream.listen((Uri? uri) {
          handleDeepLink(uri);
        }, onError: (err) {
          print('Error in listening to uri: $err');
        });
      }
      
      void handleDeepLink(Uri? uri) {
        if (uri != null) {
          print('Received URI: $uri');
          // 在这里处理URI,例如导航到特定的页面
          // Navigator.pushNamed(context, '/yourRoute', arguments: {'uri': uri.toString()});
        }
      }
      
      class MyApp extends StatelessWidget {
        @override
        Widget build(BuildContext context) {
          return MaterialApp(
            title: 'Flutter Deep Link Example',
            theme: ThemeData(
              primarySwatch: Colors.blue,
            ),
            home: MyHomePage(),
          );
        }
      }
      
      class MyHomePage extends StatefulWidget {
        @override
        _MyHomePageState createState() => _MyHomePageState();
      }
      
      class _MyHomePageState extends State<MyHomePage> {
        @override
        Widget build(BuildContext context) {
          return Scaffold(
            appBar: AppBar(
              title: Text('Flutter Deep Link Example'),
            ),
            body: Center(
              child: Text('Open your app with a deep link to see it in action!'),
            ),
          );
        }
      }
      

在这个示例中,我们:

  • 添加了uni_links依赖。
  • 配置了iOS和Android以允许应用响应深度链接。
  • 在Flutter代码中初始化了uni_links监听器,以处理应用启动时的链接以及后续接收到的链接。

你可以根据需要修改handleDeepLink函数中的逻辑,以处理不同的URI并执行相应的操作,例如导航到应用中的特定页面。

回到顶部