Flutter 實現 APP Links 與 Universal Links


#Flutter#Flutter package#uni_links#APP Link#Universal Links#Xcode#iOS#Android

Android 的 App Link 與 iOS 的 Universal Links 是可以讓手機點擊 HTTP URL Scheme (http:// or https://) 就直接開啟 APP 導向到指定頁面,也可在 APP 之間做轉跳。有別於 Deep Links 的 Custom Url Scheme 是可以避免跟其他 APP 命名衝突、或是發生使用者沒裝 APP 開啟後網頁一片白的窘境。

For Android


修改你的 AndroidManifest.xml,將要開啟 APP 的 domain + path 放到 <activity> 之中。

  • android:scheme :通常只要輸入 https,除非你特別要支援 http。
  • android:host :輸入你的 domain,如有測試環境也可一併加上,有 www 與沒 www 的算兩個 domain。
  • android:pathPrefix :因為不會是遇到網域底下所有頁面都要開啟 APP,這裡就是讓你輸入你要開啟的 path。
<!-- 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" />
    <!-- /download -->
    <data android:scheme="https" android:host="joymap.tw" android:pathPrefix="/download" />
    <data android:scheme="https" android:host="www.joymap.tw" android:pathPrefix="/download" />
    <!-- /member/news -->
    <data android:scheme="https" android:host="joymap.tw" android:pathPrefix="/member/news" />
    <data android:scheme="https" android:host="www.joymap.tw" android:pathPrefix="/member/news" />
    <!-- /store -->
    <data android:scheme="https" android:host="joymap.tw" android:pathPrefix="/store" />
    <data android:scheme="https" android:host="www.joymap.tw" android:pathPrefix="/store" />
</intent-filter>

將以下 json 放至網站 /.well-known/assetlinks.json (例如 https://joymap.tw/.well-known/assetlinks.json)
package_name 需修改成你的 package_name
sha256_cert_fingerprints 也是修改為你的指紋

[
   {
      "relation":[
         "delegate_permission/common.handle_all_urls"
      ],
      "target":{
         "namespace":"android_app",
         "package_name":"com.toby.joymap",
         "sha256_cert_fingerprints":[
            "c1:06:01:09:6c:e1:89:78:38:87:73:9a:34:1d:07:fe:bf:1b:b8:a2:e7:49:6e:11:70:d9:fb:67:22:e0:da:22"
         ]
      }
   }
]

For iOS


開啟你的 Xcode > Signing & Capabilities > + Capability > 搜尋 Associated Domains 雙擊開啟功能 > 填上你的 domain

將以下 json 放至網站 /.well-known/apple-app-site-association (例如 https://joymap.tw/.well-known/apple-app-site-association),注意檔名沒有 .json。

  • appID 格式為 {Team ID}.{Bundle Identifier}
  • paths 支援 *?NOT
{
    "applinks": {
        "apps": [],
        "details": [
            {
                "appID": "RQ8UWKNDYA.com.toby.joymap",
                "paths": ["/download", "/member/news/*", "/store/*"]
            }
        ]
    }
}

蘋果有 CDN,所以設定後需要數小時後才能動作,可以使用 https://app-site-association.cdn-apple.com/a/v1/{your_domain} 檢查是否已更新。

For Flutter


使用:https://pub.dev/packages/uni_links

新增 app_link.dart

import 'dart:async';

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

class AppLink {

  static void handleIncomingLinks(BuildContext context) {
    uriLinkStream.listen((Uri? uri) {
      if (uri != null) {
        try {
          path(context, uri);
        } catch (e) {
          print(e);
        }
      }
    }, onError: (Object err) {});
  }

  static Future handleInitialUri(BuildContext context) async {
    try {
      final uri = await getInitialUri();
      if (uri != null) {
        path(context, uri);
      }
      // ignore: empty_catches
    } on FormatException {}
  }
  static Future<bool> path(BuildContext context, Uri? uri) async {
    if (uri == null) return false;

    try {
      // 掃描支付 /download?store_id={store_id}
      if (uri.path == '/download' && uri.queryParameters['store_id'] != null) {
        _pay(context, uri);
        return true;
      }

      // 最新消息 /member/news/{id}
      if (uri.pathSegments.length == 3 && uri.pathSegments[0] == 'member' && uri.pathSegments[1] == 'news') {
        _news(context, uri);
        return true;
      }

      // 店家內頁 /store/{slug}
      if (uri.pathSegments.length == 2 && uri.pathSegments[0] == 'store') {
        _store(context, uri);
        return true;
      }
    } catch (e) {
      print(e);
    }

    return false;
  }

  static _pay(BuildContext context, Uri uri) async {
    String storeId = uri.queryParameters['store_id'] ?? '';
    // open pay page
  }

  static _news(BuildContext context, Uri uri) {
    int id = int.parse(uri.pathSegments[2]);
    // open news page
  }

  static _store(BuildContext context, Uri uri) {
    String slug = uri.pathSegments[1];
    // open store page
  }
}

在你的起始 Widget State 加入

_appLinkInit() async {
  AppLink.handleIncomingLinks(context);
  await AppLink.handleInitialUri(context);
}

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) {
    _appLinkInit();
  });
}

試試看吧


如果有安裝享樂地圖 APP 將會開啟至店家內頁,沒有的話會開啟網頁版。


參考:
https://pub.dev/packages/uni_links
https://blog.dreambreakerx.com/2018/09/universal-links/
https://medium.com/%E5%B7%A5%E7%A8%8B%E5%B8%AB%E6%B1%82%E7%94%9F%E6%8C%87%E5%8D%97-sofware-engineer-survival-guide/%E6%B7%BA%E8%AB%87app-link%E8%88%87deep-link%E5%AF%A6%E4%BD%9C-995734a11889