yii.
← Writing
Flutter 技術 · · 8 min read

Flutter 實作 DeepLink 完整指南 ⎮ Part 4: 適配與掌握社交平台

不可忽視的 Deep Link 體驗

如果帳戶不是 Medium Member 的朋友,可以點擊我的開放連結瀏覽文章。

社交平台的適配#

產品使用上經常會有個情境,邀請連結或商品連結需要分享給朋友,這時候的管道通常會是 Line、Facebook、IG 等等,經由聊天室轉發,會有幾個期望情境或流程,不過有時候現實運作卻不一樣。以 IG、Facebook 來說不同系統的手機就有不同效果,使用 Http Link 的 Android App Link、iOS Universal Link,可能會遇到幾個場景

  1. Android 可以開啟連結並開啟 APP 進行操作,但是 iOS 就不同了,會需要有一個中繼頁面,讓用戶點擊按鈕後打開 DeepLink 連結才能正常,否則在 IG 中會不被辨識,點擊後會閃一下然後被擋掉
  2. 不同手機系統,有些連結能正常運作、跳轉,有些連結卻只會開啟商店

期望:

  1. 沒有安裝的用戶,跳轉到商店引導下載
  2. 有下載且登入的用戶,可以開啟 App 並進入到指定的商品頁面,甚至是直接帶入或顯示相關資訊

原因:在每個社交產品的運作上,開啟網頁、連結都會有自己定義的行為,可能會需要在 URI 上添加一些特定參數,給平台進行驗證並協助開啟。例如:Line 上需要添加 ?openExternalBrowser=1

iOS#

Instagram#

  1. iOS 裝置在 IG 無法直接點擊連結就開啟 App,DeepLink 會被 IG 在內部瀏覽器擋掉
  2. 必須先用 In-App Browser 開啟一個中繼網頁,上面點擊 DeepLink 按鈕打開實際網址,Deep Link 才能有效地開啟 App,這時候才可以正常處理連結,並跳轉到指定頁面。(例如:可以用 LinkTree 當成中繼頁面)

Line#

  1. In-App Browser 都會讓 DeepLink 失效,除非在 query 添加 openExternalBrowser=1
https://hello.yiichen.com/product/xxx?openExternalBrowser=1

Android#

Instagram#

  1. 可以直接點擊 DeepLink 就開啟 app,但如果打開中繼頁面再點擊 DeepLink 跳轉,這樣會失效,即使已經下載了 App 仍然會跳轉到 Play Store商店
  2. 用戶必須事先開過 App 在背景運行,如果沒有先開啟,經過 Deep link 重新啟動 APP 的情況, 不會收到 DeepLink 相關資訊,也就只能到 / Route path

以上問題可以透過三方服務來協助我們,例如: AppsFlyer、Branch。使用 Custom Scheme 的連結可以正常運作,幫我們開啟對應的 APP


第三方服務的優勢#

  • Useful when you don’t have a domain 沒有網域
  • Fallback mechanisms 觸發機制
  • Link shortening 短連結
  • Analytics 數據分析
  • Attribution tracking 資料追蹤
  • Deferred deep linking 延遲深度鏈接:針對剛安裝應用的用戶,確保來源並跳轉到指定頁面或是蒐集相關數據

AppsFlyer#

appsflyer-flutter-plugin/doc/DeepLink.md at master · AppsFlyerSDK/appsflyer-flutter-pluginFlutter Plugin for AppsFlyer SDK. Contribute to AppsFlyerSDK/appsflyer-flutter-plugin development by creating an…github.com

Setup (Android)#

設定 AndroidManifest.xml,vibz-app.onelink.me domain 為 https,也就是用戶點擊時的公開連結,需要透過它開啟 APP,並指向到我們內部設定的自定義 URI

<!-- This link is from AppsFlyer. It should be added. -->
<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="vibz-app.onelink.me"
        android:pathPrefix="/PqsT" />

</intent-filter>

Android initial setupAt a glance : The initial app setup enables the marketer to create links that send existing app users directly into the…dev.appsflyer.com

自定義的 URI 設置,實際最終跳轉的通道

<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="vibz"
        android:host="vibz.cool" />

</intent-filter>

Setup (iOS)#

iOS initial setupAt a glance : The initial app setup enables the marketer to create links that send existing app users directly into the…dev.appsflyer.com

AppsFlyer Console Setting#

避免使用 http、https 開頭的 scheme,很常會在社群平台被擋下或是影響跳轉,例如:IG 點擊連結後會跳轉到 404 網頁。可以為品牌定義自己的 scheme 來使用。

設置 https scheme 會無法開啟商店或是 APP

幫品牌定義了自己的 scheme,在 IG 點擊連結後就能正常開啟 OneLink 的引導網頁,點擊後即可開啟 APP 商店。

Smart Using AppsFlyer#

重復使用品牌的 AppsFlyer 格式但不生成新的子連結 ID,使用一對多的方式,可以更改指定參數來完成,不需要為每個行為都生成一個新的 OneLink。因此可避免創建的 API 使用流量,通常可能會想到透過後端去生成所有連結,但每產一個都是成本,依照方案有不同限制。而使用自定義參數去動態配置便能解決此問題。

通常格式都會是品牌名稱 vibz 加上 .onelink.me,後面為 Templaste ID,最後面可以加上獨立 ID,但這裡自定義我只需要 Template 辨識就好。

https://vibz-app.onelink.me/PqsT
  • af_dp → fallback 連結,真正 APP 有設定的 Link 格式與內容,也是主要需要修改的部分
  • af_force_deeplink → iOS 需要設置的參數,強制執行 DeepLink 互動,否則有可能在有安裝 APP 的情境下還是開啟商店,設置為 true
// Normal
<https://vibz-app.onelink.me/PqsT?af_dp=vibz://share.vibz.cool/campaigns/dthm31/claim?claim_code=bmljZQ==&af_force_deeplink=true>

// Wrap own short-link
<https://link.vibz.cool/link/gXhY1CKsS>

AppsFlyer API Access#

也可以直接請求 AppsFlyer API 生成 AppsFlyer 連結,也是一個選項,根據實際需求進行選擇與實作

{
  "af_ad": "yellow_bananas",
  "af_adset": "my_adset",
  "af_android_url": "<https://feedme.ca/buybananas>",
  "af_channel": "my_channel",
  "af_dp": "afbasicapp://mainactivity",
  "af_ios_url": "<https://feedme.ca/buybananas>",
  "c": "my_campaign",
  "deep_link_value": "bananas",
  "deep_link_sub1": 10,
  "is_retargeting": true,
  "pid": "my_media_source_SMS"
}

Create OneLink attribution linkExample: Feed Me, a grocery delivery service, wants to send a personalized link via SMS to existing customers to…dev.appsflyer.com


Flutter Development#

可統一使用 onDeepLinking(),捕捉所有情境:

  1. 使用者點擊 OneLink 短 URL。
  2. iOS 通用連結/ Android 應用程式連結(用於深度連結)或延遲的深度連結會觸發 SDK。
  3. SDK觸發 didResolveDeepLink 方法,將深度連結結果物件傳遞給使用者。
  4. onDeepLinking 方法使用包含 deep_link_value 和其他參數的深層連結結果物件來為使用者建立個人化體驗,這是 OneLink 的主要目標。
appsflyerSdk.onDeepLinking((DeepLinkResult dp) {
  switch (dp.status) {
    case Status.FOUND:
      debugPrint(dp.deepLink.toString());
      break;
    case Status.NOT_FOUND:
      debugPrint("deep link not found");
      break;
    case Status.ERROR:
      debugPrint("deep link error: ${dp.error}");
      break;
    case Status.PARSE_ERROR:
      debugPrint("deep link status parsing error");
      break;
  }
}

// dp.deepLink
{
  "click_http_referrer": "",
  "af_sub1": "",
  "af_sub2": "",
  "af_sub3": "",
  "af_sub4": "",
  "af_sub5": "",
  "deep_link_value": "vibz://share.vibz.cool/campaigns/nhdwc_1st/claim",
  "campaign": "",
  "campaign_id": "",
  "match_type": "probabilistic",
  "media_source": "",
  "is_deferred": true
}

由 DeepLink 類別儲存連結相關資訊:

class DeepLink {

    DeepLink(this._clickEvent);

    final Map<String , dynamic> _clickEvent;
    Map<String , dynamic> get clickEvent => _clickEvent;
    String? get deepLinkValue =>  _clickEvent["deep_link_value"] as String;
    String? get matchType =>  _clickEvent["match_type"] as String;
    String? get clickHttpReferrer =>   _clickEvent["click_http_referrer"] as String;
    String? get mediaSource =>  _clickEvent["media_source"] as String;
    String? get campaign =>  _clickEvent["campaign"] as String;
    String? get campaignId =>   _clickEvent["campaign_id"] as String;
    String? get afSub1 => _clickEvent["af_sub1"] as String;
    String? get afSub2 =>  _clickEvent["af_sub2"] as String;
    String? get afSub3 => _clickEvent["af_sub3"] as String;
    String? get afSub4 =>  _clickEvent["af_sub4"] as String;
    String? get afSub5 =>   _clickEvent["af_sub5"] as String;
    bool get isDeferred =>  _clickEvent["is_deferred"] as bool;

    @override
    String toString() {
        return 'DeepLink: ${jsonEncode(_clickEvent)}';
    }
    String? getStringValue(String key) {
        return _clickEvent[key] as String;
    }
}

提醒:如果有在屬性 deep_link_value 或其他欄位放入 Url 資料,通常 & 在 HTML 編碼為 &amp ,如果要直接使用的話需要清理一下。可以簡單透過 String replace api 處理

// For String
final cleanUri = link.replaceAll('amp;', '');

// For Uri
final cleanUri = uri?.replace(query: uri.query.replaceAll('amp;', ''));

appsflyer-flutter-plugin/doc/DeepLink.md at master · AppsFlyerSDK/appsflyer-flutter-pluginFlutter Plugin for AppsFlyer SDK. Contribute to AppsFlyerSDK/appsflyer-flutter-plugin development by creating an…github.com

Deferred Deep Linking#

如果應用未安裝怎麼辦?這是使用延遲深度連結的地方。當應用程式未安裝時,點擊連結會將使用者引導至商店下載應用程式。延遲深層連結將深層連結過程推遲或延遲到應用程式下載後,並確保用戶安裝後到達應用程式中的正確位置。

使用 onInstallConversionData() 取得安裝應用後的狀態,包含一些基礎資訊。

appsflyerSdk.onInstallConversionData((res) {
  debugPrint('DeepLink - AppsFlyer - onInstallConversionData - $res');
});

可惜的是我們目前只能抓到幾種資料:

  1. is_first_launch: 是否第一次開啟。代表安裝後的第一次啟動
  2. install_time: 安裝時間
{
  "status": "success",
  "payload": {
    "is_first_launch": true,
    "install_time": "2024-06-02 15:33:20.791",
    "af_message": "organic install",
    "af_status": "Organic"
  }
}

沒有先前在連結提供的自定義資訊,因此我們無法精準定位到使用者的意圖。


可以直接使用 Appsflyer 或平台提供的 Deep Link 連結,但如果想要有品牌自定義的 URI,可自行包裝,進行連結跳轉。

以下使用我的專案範例作為講解:

1️⃣ 原始 Custom Scheme Link

vibz://share.vibz.cool/campaigns/vibz-drop-42/claim

2️⃣ 精緻包裝的 AppsFlyer 連結
https://vibz-link.onelink.me/0Smm?af_og_title=現在參與活動來贏得 回訊息啦 VIBE 風格徽章 — VIBZ&af_og_description=在 VIBZ 上參與限時活動來蒐集 回訊息啦 VIBE 風格徽章!&af_og_image=https://production-vibz-core-campaignimagebucketaa7fff6e-v5kjcb897wfy.s3.ap-northeast-1.amazonaws.com%2Fvibz-drop-42.png&af\_param\_forwarding=true&af\_force\_deeplink=true&af\_dp=vibz://share.vibz.cool/campaigns/vibz-drop-42/claim&deep\_link\_value=vibz://share.vibz.cool/campaigns/vibz-drop-42/claim&af\_web\_dp=https://www.vibz.cool

3️⃣ 公開分享連結

https://link.vibz.cool/link/86XhztgjJ

參數資料:

  • af_og_title → 連結顯示標題
  • af_og_description → 連結描述
  • af_og_image → 連結圖像
  • af_deeplink → true
  • af_param_forwarding → true
  • af_force_deeplink → 允許 DeepLink 運作,針對一些內部瀏覽器。true
  • af_dp → 實際 DeepLink 溝通連結。APP 端使用 Custom Scheme
  • deep_link_value → 實際 DeepLink 溝通連結。APP 端使用 Custom Scheme
  • deep_link_sub1 → 自定義資訊
  • af_web_dp → 網頁連結,可是制為官方連結

實作細節#

實作過後,得出 Deep Link 完整情境的掌握需要 AppLinks 和 AppsFlyer 協作

  1. AppLinks 使用 getInitialAppLink() 捕捉 App 完全關閉(terminated)時觸發的 Link
  2. AppsFlyer 負責處理一般情境下的 Link,尤其是處理在第三方社交平台的操作,主要針對 Custom Scheme 連結,當 APP 在前景或背景都能捕捉
  3. APP 內只使用了自己的 Custom Scheme,沒有 Http Scheme

Conclusion#

在本文中我們說明 Deep Link 與社交平台的互動細節,透過不同的實作方式讓連結能完整的被 APP 掌握,在裝置的各種運行情境下都能正常運作,最終協助用戶解決問題,有效提升對應用的好感度。

有關 Deep Link 系列的文章就到這裡告一段落了,如果想熟悉每個環節的話,可以點擊以下的其他連結:

當然文章主要是希望給予讀者一點啟發,不需要拘泥於作法。非常期待大家的踩坑分享與開發心得!


本文原刊登於 Medium。