Universal Links & App Links 完全理解:仕組みから実装、デバッグまで
はじめに
Webサイトのリンクをクリックした時、ブラウザが開く代わりに特定のアプリがスッと起動する。この快適なユーザー体験を実現するのが、Universal Links (iOS) と App Links (Android) です。これらは単なる便利な機能ではなく、現代のアプリ開発においてユーザーエンゲージメントを高めるための必須技術となっています。
しかし、その設定はWebサーバーとアプリの両方にまたがり、プラットフォームごとの細かな違いもあって、多くの開発者がつまずきやすいポイントでもあります。特にFlutterのようにクロスプラットフォームで開発していると、両方の知識が求められ、混乱しがちです。
この記事でわかること
この記事では、Universal LinksとApp Linksの根本的な「仕組み」から、Flutterでの具体的な「実装方法」、そして最も重要な「トラブルシューティング」までを、一気通貫で解説します。
最終的には、単に実装ができるだけでなく、「なぜそうするのか」を他人に説明できるレベルの深い理解を目指します。
対象読者
- プラットフォームの知識をもっと知りたいFlutterアプリエンジニア
- Universal Links / App Links の実装でハマった経験がある方
- サービスのグロース施策として、ディープリンクの導入を検討している方
1. ディープリンクの基礎知識
まず、Universal Links / App Linksがどのような課題を解決するために生まれたのか、その背景から見ていきましょう。
ディープリンクとは? なぜ必要なのか?
ディープリンク(Deep Link)とは、Webサイトにおけるハイパーリンクのアプリ版です。Webページが https://example.com/path/to/page のようなURLで特定のページを示すように、ディープリンクは myapp://products/123 のようなURIや https://example.com/products/123 のようなURLを使って、アプリ内の特定の画面へ直接ユーザーを誘導する仕組みを指します。
これがなければ、ユーザーはアプリのトップページから目的の画面まで、手動で何度もタップして遷移しなければなりません。ディープリンクは、この手間を省き、以下のような価値を提供します。
- ユーザー体験の向上: 通知やWeb広告、メールマガジンなどから、ユーザーが見たいコンテンツに直接アクセスできるため、満足度が向上します。
- マーケティング施策の強化: 広告キャンペーンの効果測定や、特定の機能へユーザーを誘導するオンボーディングなどで活用できます。
従来のディープリンク:カスタムURLスキームの問題点
Universal Links / App Linksが登場する前は、「カスタムURLスキーム」という技術が主流でした。これは twitter:// や youtube:// のように、アプリが独自のURIスキームを宣言する方式です。
シンプルで実装しやすい反面、重大な問題を抱えていました。
セキュリティリスク
カスタムURLスキームは、誰でも自由に宣言できます。例えば、ある銀行アプリが mybank:// というスキームを使っていたとします。悪意のある第三者が、同じ mybank:// を使う偽物のアプリを作成し、ユーザーのデバイスにインストールさせることが可能です。もしユーザーが mybank:// で始まるリンクをクリックした場合、OSはどちらのアプリを起動すべきか判断できず、最悪の場合、偽物のアプリが起動して情報を盗まれる危険性がありました。
アプリの重複
セキュリティリスクと関連して、複数の正規アプリが偶然同じスキームを名乗ってしまう可能性もあります。この場合、OSはユーザーに「どちらのアプリで開きますか?」という選択ダイアログを表示します。これはユーザー体験を損なうだけでなく、開発者が意図した通りの動作を保証できません。
フォールバックの欠如
最大の問題は、アプリがインストールされていない場合に何も起きないことです。リンクはただの「死んだリンク」となり、ユーザーは放置されてしまいます。Webページにフォールバックさせるような仕組みは標準では提供されていませんでした。
Universal Links / App Links が解決すること
これらの問題を解決するために、AppleとGoogleはそれぞれ Universal Links (iOS) と App Links (Android) を導入しました。これらはHTTP/HTTPSの標準的なWebリンクを利用する、新しい形のディープリンクです。
最大の特徴は、Webサイトのドメインとアプリの所有権を、サーバーとアプリの両方で検証し、安全に関連付ける点にあります。
-
所有権の証明による高いセキュリティ: Webサーバーに
apple-app-site-associationやassetlinks.jsonといった設定ファイルを配置することで、「このドメインは、このアプリと本当に関連しています」ということを暗号学的に証明します。これにより、カスタムURLスキームのようななりすましは不可能になります。 - シームレスなユーザー体験: 所有権が検証されたリンクは、OSによって「信頼できる」と見なされます。そのため、ユーザーに選択を求めることなく、常に正しいアプリが直接起動します。
- 確実なフォールバック: もしアプリがインストールされていない場合でも、リンクは通常のURLであるため、自動的にブラウザでそのWebページが開かれます。ユーザー体験が途切れることがありません。
このように、Universal Links / App Linksは、従来のディープリンクが抱えていた問題を解決し、より安全で信頼性の高い体験を提供するための仕組みなのです。
2. Universal Links (iOS) の仕組み
ここからは、iOSのUniversal Linksがどのような流れで実現されているのかを、コンポーネントごとに詳しく見ていきましょう。
全体像:ユーザーがリンクをクリックしてからアプリが起動するまで
まずは、一連のフローを俯瞰してみます。
-
開発者の準備:
-
サーバー: Webサイトのドメイン (
https://example.com) に、apple-app-site-association(AASA) というJSONファイルを設置します。 -
アプリ: Xcodeプロジェクトで、「このアプリは
example.comに関連しています」というAssociated Domainsの設定を有効にします。
-
サーバー: Webサイトのドメイン (
-
OSによる事前検証:
- ユーザーがアプリをApp Storeからインストール、またはアップデートします。
- iOSは、アプリに設定されたAssociated Domainsを検知し、そのドメインに置かれたAASAファイルを自動的にダウンロードしに行きます。
- ダウンロードしたAASAファイルの内容が正当であるか(署名やフォーマットが正しいか)を検証し、問題なければそのドメインとアプリの関連付けを承認します。
-
ユーザーの操作:
- ユーザーがメッセージアプリやブラウザなどで、
https://example.com/products/123のようなURLをタップします。
- ユーザーがメッセージアプリやブラウザなどで、
-
OSによる起動判断:
- iOSは、タップされたURLのドメイン (
example.com) が、事前検証で承認済みのドメインであるかを確認します。 - 承認済みの場合、AASAファイルに記述されたパスパターン (
/products/*など) と、タップされたURLのパス (/products/123) が一致するかを照合します。 - 一致すれば、Safariを起動せず、直接アプリを起動します。このとき、タップされたURLの情報がアプリに渡されます。
- ドメインが未承認、またはパスが一致しない場合は、通常通りSafariでそのURLを開きます。
- iOSは、タップされたURLのドメイン (
この「OSによる事前検証」が、Universal Linksのセキュリティと信頼性を支える最も重要な部分です。
サーバー側の設定:apple-app-site-association (AASA) ファイル
このファイルは、Webサーバー側で行う唯一かつ最も重要な設定です。Webサイトの所有者だけがファイルを設置できる、という事実をもって、ドメインの所有権を証明します。
{
"applinks": {
"details": [
{
"appIDs": ["ABCDE12345.com.example.myapp"],
"components": [
{
"/": "/articles/*",
"comment": "記事ページにマッチ"
},
{
"/": "/users/?*",
"exclude": true,
"comment": "特定のユーザーページを除外"
}
]
}
]
},
"webcredentials": {
"apps": ["ABCDE12345.com.example.myapp"]
}
}
applinks の詳細
-
appIDs: Universal Linksを有効にするアプリのIDを配列で指定します。[<Team ID>.<Bundle ID>]の形式です。 -
components: URLのパスに基づいて、アプリで開くルールを詳細に定義します。-
/: パスのルートを指定します。このキーに対して、マッチさせたいパスパターンを記述します。-
*: 任意の文字列(0文字以上)にマッチします。 -
?: 任意の1文字にマッチします。
-
-
exclude:trueに設定すると、そのパターンに一致するURLをUniversal Linksの対象から除外できます。 -
comment: 開発者向けのコメントです。
-
ファイルの配置場所と注意点
-
ファイル名:
apple-app-site-associationという名前でなければなりません。.json拡張子は付けません。 -
配置場所:
-
https://<ドメイン>/.well-known/apple-app-site-association(推奨) https://<ドメイン>/apple-app-site-association
-
-
サーバー要件:
- HTTPSで配信されている必要があります。HTTPでは機能しません。
- リダイレクトは許可されません。iOSは指定されたURLに直接アクセスしようとします。
-
Content-Typeヘッダーはapplication/jsonであることが推奨されます。 - ファイルサイズは128KB未満である必要があります。
アプリ側の設定:Associated Domains の有効化
アプリ側では、Xcodeで簡単な設定を行うだけです。
- Xcodeでプロジェクトを開き、ターゲットを選択します。
- 「Signing & Capabilities」タブを開きます。
- 「+ Capability」をクリックし、「Associated Domains」を追加します。
- 表示された入力欄に、
applinks:というプレフィックスを付けて、AASAファイルを設置したドメインを登録します。- 例:
applinks:www.example.com
- 例:
これで、アプリは「私は www.example.com と関連があります」とOSに伝える準備ができました。
OSによる検証プロセス
このプロセスは自動的に行われ、開発者が直接操作することはありませんが、トラブルシューティングのためには理解しておくことが不可欠です。
AASAファイルの取得とCDNキャッシュ
- iOSは、アプリのインストール時やアップデート時に、Associated Domainsに登録されたドメインに対してAASAファイルの取得を試みます。
- このとき、iOSは直接あなたのサーバーにアクセスするとは限りません。Appleが運用する**専用のCDN(コンテンツ配信ネットワーク)**を介してファイルを取得します。
- そのため、サーバー上でAASAファイルを更新しても、変更がiOSデバイスに反映されるまでには時間がかかる場合があります(最大で24時間程度と言われています)。これは、CDNのキャッシュが更新されるのを待つ必要があるためです。
- また、iOSはアプリのインストール時だけでなく、定期的(およそ週に一度と言われています)にAASAファイルを再検証します。これにより、アプリのアップデートをせずとも、サーバー側の設定変更でUniversal Linkの挙動を更新することが可能です。ただし、この定期的なチェックも同様にAppleのCDNを介して行われるため、即時の反映は保証されません。
アプリインストール・アップデート時の検証フロー
- アプリのインストール: ユーザーがApp Storeからアプリをインストールします。
- ドメインの抽出: iOSはアプリのプロビジョニングプロファイルからAssociated Domainsの情報を読み取ります。
- AASAファイルの取得: iOSは(AppleのCDNを介して)各ドメインのAASAファイルを取得します。
- 署名の検証: ファイルが正しく署名されているか、改ざんされていないかを確認します。
- 関連付けの承認: 検証に成功すると、iOSはデバイス内でそのドメインとアプリの関連付けを記録・承認します。
この検証が失敗すると、Universal Linksは機能しません。
パス判定の内部ロジック
ユーザーがリンクをクリックした際、OSは承認済みのAASAファイルに記載されたcomponentsのルールに基づき、アプリを起動するかどうかを決定します。この判定は、以下の優先順位で厳密に行われます。
-
excludeによる除外が最優先- クリックされたURLのパスが、
exclude: trueが設定されたcomponentsのパターンに一致する場合、他のどのパターンに一致していても、Universal Linkの対象外となります。Safariで開かれます。
- クリックされたURLのパスが、
-
より具体的なパスパターンが優先
- 複数のパターンに一致する場合、より長く、より具体的なパスパターンが優先されます。
- 例えば、
/articles/2024/*と/articles/*の両方に一致するURLの場合、より具体的な/articles/2024/*のルールが適用されます。
-
components配列の順序- パスの具体性が同じ場合、
components配列の前方(上)に記述されているルールが優先されます。
- パスの具体性が同じ場合、
-
クエリパラメータとフラグメントの扱い
- パターンマッチングは、URLのパス部分に対してのみ行われます。
?以降のクエリパラメータや、#以降のフラグメントは判定には影響しません。 - ただし、パスパターンの中で
?や*を使って、クエリパラメータを含んだURL全体にマッチさせることは可能です。(例:/search?query=*)
- パターンマッチングは、URLのパス部分に対してのみ行われます。
このロジックを理解することで、意図しない挙動(アプリで開いてほしいのに開かない、またはその逆)の原因を特定しやすくなります。
ユーザーによる上書き設定と挙動
- ユーザーがUniversal Linkによってアプリで開かれた後、画面右上に表示される「example.com >」というボタンをタップすると、そのリンクはSafariで開かれます。
- その後、Safariで表示されたページで「開く」を選択すると、再びアプリで開くようになります。
- このように、ユーザーはリンクごとにアプリで開くか、ブラウザで開くかを選択でき、その選択はOSによって記憶されます。開発者はこのユーザーの選択を強制的に変更することはできません。
3. App Links (Android) の仕組み
続いて、AndroidのApp Linksです。基本的な考え方はiOSのUniversal Linksと共通していますが、検証に使われる情報や設定方法に違いがあります。
全体像:ユーザーがリンクをクリックしてからアプリが起動するまで
-
開発者の準備:
-
サーバー: Webサイトのドメイン (
https://example.com) の/.well-known/ディレクトリに、assetlinks.jsonというJSONファイルを設置します。 -
アプリ:
AndroidManifest.xmlで、特定のURLを処理するためのインテントフィルターを定義し、android:autoVerify="true"属性を追加します。
-
サーバー: Webサイトのドメイン (
-
OSによる事前検証:
- ユーザーがアプリをインストールします。
- Android OSは、マニフェストの
autoVerify="true"を検知します。 - OSは、インテントフィルターに記述されたドメインにアクセスし、
assetlinks.jsonファイルをダウンロードして検証します。この検証は、ファイルに記述されたアプリの署名証明書のフィンガープリントと、実際にインストールされたアプリの署名を照合することで行われます。
-
ユーザーの操作:
- ユーザーが
https://example.com/products/123のようなURLをタップします。
- ユーザーが
-
OSによる起動判断:
- Android OSは、タップされたURLを処理できるアプリを探します。
-
assetlinks.jsonの検証に成功しているアプリが唯一の候補となり、ユーザーに選択を求めることなく、直接そのアプリが起動します。 - 検証に失敗した場合や、同じURLを処理できる他のアプリがある場合は、ユーザーに選択ダイアログ(Disambiguation Dialog)が表示されます。
サーバー側の設定:assetlinks.json ファイル
このファイルは、アプリとWebサイトの関連付けを宣言するもので、デジタルアセットリンク (Digital Asset Links) という仕組みに基づいています。
[{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.myapp",
"sha256_cert_fingerprints":
["1A:2B:3C:...:E0:F1"]
}
}]
relation と target の詳細
-
relation: どのような権限を委譲するかを定義します。App Linksの場合はdelegate_permission/common.handle_all_urlsを指定します。これは「このWebサイトの全てのURLを、指定されたターゲットアプリで処理することを許可する」という意味です。 -
target: 権限を委譲する対象のアプリを指定します。-
namespace:android_appを指定します。 -
package_name: アプリのパッケージ名(build.gradleのapplicationId)を指定します。 -
sha256_cert_fingerprints: 最も重要な項目です。アプリの署名に使われる証明書のSHA-256フィンガープリントを配列で指定します。これにより、正規の開発者によって署名されたアプリであることを保証します。Google Play Consoleから取得するか、keytoolコマンドで確認できます。
-
ファイルの配置場所と注意点
-
ファイル名:
assetlinks.jsonという名前でなければなりません。 -
配置場所:
https://<ドメイン>/.well-known/assetlinks.jsonのみに配置します。 -
サーバー要件:
- HTTPSで配信されている必要があります。
- リダイレクトは許可されません。
-
Content-Typeヘッダーはapplication/jsonである必要があります。 - robots.txtで
/.well-known/へのクロールが禁止されていないことを確認してください。
アプリ側の設定:インテントフィルターと autoVerify
AndroidManifest.xml 内の、アプリを起動させたいActivityに対して、インテントフィルターを設定します。
<activity ...>
<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="www.example.com" />
</intent-filter>
</activity>
-
android:autoVerify="true": この属性がApp Linksを有効にするための鍵です。OSに対して、このインテントフィルターに関連付けられたホストのassetlinks.jsonを自動的に検証するように指示します。 -
<action>:VIEWアクションを受け取ることを示します。 -
<category>:DEFAULTとBROWSABLEを指定します。BROWSABLEは、ブラウザからリンクがクリックされたときにアプリを起動するために必須です。 -
<data>: どのURLに応答するかを定義します。android:schemeにhttpとhttpsの両方を指定し、android:hostにドメイン名を指定します。パスを限定したい場合はandroid:pathPatternなども使用できます。
OSによる検証プロセス
assetlinks.json の取得と検証タイミング
-
検証はアプリのインストール時に実行されます。Android 12以降では、
adbコマンドを使ったり、設定画面から手動で再検証をトリガーすることもできます。 - OSは、マニフェスト内の
autoVerify="true"が付いたインテントフィルターを見つけると、そこに記載されたすべてのホストに対してassetlinks.jsonの取得を試みます。
デジタルアセットリンクの検証フロー
- インテントフィルターのスキャン: OSはインストールされたアプリのマニフェストをスキャンします。
-
assetlinks.jsonの取得:autoVerify="true"を持つインテントフィルターのホストからassetlinks.jsonをダウンロードします。 -
署名の照合: ダウンロードしたファイル内の
sha256_cert_fingerprintsと、インストールされたアプリの署名証明書のフィンガープリントを比較します。 - 関連付けの承認: 両者が一致すれば、OSはそのドメインとアプリの関連付けを承認し、App Linkとして登録します。
App Link状態の確認方法
デバイス上でApp Linkの検証が成功したかどうかは、以下の方法で確認できます。
- 設定アプリ: [設定] > [アプリ] > [(対象アプリ)] > [デフォルトで開く] を開きます。「このアプリで対応リンクを開く」がオンになっており、「確認済みのリンク」としてドメインが表示されていれば成功です。
-
adbコマンド:上記コマンドを実行し、
adb shell dumpsys package d | grep -A 2 com.example.myappdomain.nameのステータスがverifiedとなっていれば検証成功です。
4. Universal Links と App Links の比較
これまで見てきたように、Universal LinksとApp Linksは同じ目的を持ちながら、その実現方法はプラットフォームごとに異なります。ここで両者の違いを整理しておきましょう。
| 項目 | Universal Links (iOS) | App Links (Android) |
|---|---|---|
| 設定ファイル |
apple-app-site-association (拡張子なし) |
assetlinks.json |
| ファイル配置場所 |
/.well-known/ または / (ルート) |
/.well-known/ のみ |
| アプリIDの指定 | Team ID + Bundle ID | Package Name |
| アプリ正当性の検証 | Apple Developerアカウントによる暗黙的な信頼 | 署名証明書のSHA-256フィンガープリント |
| パスの指定方法 | AASAファイル内のcomponentsで柔軟に指定 |
AndroidManifest.xmlのインテントフィルターで指定 |
| 検証タイミング | アプリのインストール/アップデート時、および定期的(週1回程度) | アプリのインストール時 |
| OSによるキャッシュ | AppleのCDNによってキャッシュされる | OSによるキャッシュ。手動での再検証も可能 |
| ユーザーによる制御 | リンク長押しや右上のボタンでSafariで開く選択が可能 | 設定アプリからデフォルト動作の変更が可能 |
特に重要な違いは、アプリの正当性を何で検証しているかです。iOSがApple Developer Programという閉じられたエコシステムを信頼の基盤にしているのに対し、Androidはオープンなエコシステムであるため、暗号学的な署名(フィンガープリント)によって厳密にアプリの所有者を検証しています。
5. Flutterでの実装方法
いよいよ、Flutterアプリでこれらの機能を実装する方法を見ていきましょう。uni_links は現在メンテナンスが停止しているため、代替として継続的にメンテされている app_links パッケージを使用します。app_links はプラットフォーム間の差異を吸収し、初回リンクとランタイムのリンク受信を簡潔に扱えます。* 追記(2025/10/04): osakiさん、uni_linksのdiscontinuedのご指摘ありがとうございました!
必要なパッケージの紹介
まずは、pubspec.yamlにapp_linksパッケージを追加します。
dependencies:
flutter:
sdk: flutter
app_links: ^6.4.1 # 最新版をpub.devで確認してください
このパッケージは、以下の機能を提供します。
- アプリ起動のきっかけとなった最初のリンクを取得する
- アプリが既に起動している状態で、新しいリンクを受け取ったことを検知するストリーム
iOS向けの設定手順まとめ
-
Associated Domainsの有効化
- Xcodeを開き、「Signing & Capabilities」タブで「Associated Domains」を追加します。
-
applinks:your.domain.comのように、AASAファイルを設置したドメインを登録します。
-
サーバーにAASAファイルを設置
- 前述の「サーバー側の設定」セクションを参考に、
apple-app-site-associationファイルを作成し、サーバーにアップロードします。
- 前述の「サーバー側の設定」セクションを参考に、
これだけで、iOS側の設定は完了です。app_links パッケージがネイティブの処理を自動でハンドリングしてくれます。
Android向けの設定手順まとめ
-
インテントフィルターの設定
-
android/app/src/main/AndroidManifest.xmlを開き、<activity>タグ内にインテントフィルターを追加します。
AndroidManifest.xml<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.domain.com" /> </intent-filter> -
-
サーバーに
assetlinks.jsonファイルを設置- 前述の「サーバー側の設定」セクションを参考に、
assetlinks.jsonファイルを作成し、サーバーにアップロードします。リリース用の署名証明書のフィンガープリントを使うことを忘れないでください。
- 前述の「サーバー側の設定」セクションを参考に、
アプリ起動時にリンク情報を取得するコード例
app_links を使って、アプリがリンクによって起動された際の処理を実装します。main.dartや、アプリの初期化を行うウィジェットのinitStateなどに記述するのが一般的です。
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:app_links/app_links.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatefulWidget {
@override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late final AppLinks _appLinks;
StreamSubscription<Uri>? _sub;
@override
void initState() {
super.initState();
_appLinks = AppLinks();
_handleIncomingLinks();
_handleInitialLink();
}
@override
void dispose() {
_sub?.cancel();
super.dispose();
}
// アプリが起動している間に新しいリンクを受け取った場合
void _handleIncomingLinks() {
_sub = _appLinks.uriLinkStream.listen((Uri uri) {
if (!mounted) return;
print('Got new link: $uri');
// ここで受け取ったURIに基づいて画面遷移などの処理を行う
// e.g. Navigator.of(context).pushNamed(uri.path);
}, onError: (err) {
print('Got error: $err');
});
}
// アプリがリンクによって起動された場合
Future<void> _handleInitialLink() async {
try {
final initialUri = await _appLinks.getInitialAppLink();
if (initialUri != null) {
print('Got initial link: $initialUri');
// ここで受け取ったURIに基づいて画面遷移などの処理を行う
}
} on PlatformException {
print('Failed to get initial link.');
} on FormatException {
print('Malformed initial link.');
}
}
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('App Links Example'),
),
body: Center(
child: Text('Listening for links...'),
),
),
);
}
}
このコードは、2つのケースを処理しています。
-
_handleInitialLink: アプリが終了している状態から、リンクによって起動された場合に、その最初のリンクを取得します。 -
_handleIncomingLinks: アプリが既にバックグラウンドなどで起動している状態で、新しいリンクがクリックされた場合に、そのリンクをストリームで受け取ります。
これらのハンドラ内で、受け取ったUriオブジェクトのpathやqueryParametersを解析し、適切な画面に遷移させるロジックを実装することになります。
6. トラブルシューティングとデバッグ
Universal LinksとApp Linksの実装は、多くの落とし穴があるため、一度で完璧に動作することは稀です。ここでは、問題が発生したときに確認すべき点と、デバッグに役立つツールを紹介します。
よくある失敗パターン
-
サーバー設定のミス
-
Content-Typeがapplication/jsonになっていない。 - AASAファイルに
.json拡張子を付けてしまっている (iOS)。 - HTTPSではなくHTTPで配信している。
- リダイレクトが発生している。
- CDNのキャッシュが更新されておらず、古い設定ファイルを参照している (iOS)。
-
-
アプリ設定のミス
- Team ID, Bundle ID, Package Nameが間違っている。
-
sha256_cert_fingerprintsがデバッグ用のものになっている (Android)。 -
AndroidManifest.xmlのautoVerifyをtrueにし忘れている (Android)。 - Associated Domainsのプレフィックスが
applinks:になっていない (iOS)。
-
テスト方法のミス
- Safariのアドレスバーに直接URLをペーストしてもUniversal Linkは動作しない(iOS 10以降)。メモアプリやメッセージアプリなどからリンクをタップする必要があります。
iOSのデバッグ方法
AASAファイルのバリデーション
Appleが公式に提供しているわけではありませんが、サードパーティ製の優れたオンラインバリデーターが存在します。
- AASA Validator: Branch社のツール。ドメインを入力するだけで、AASAファイルの内容をチェックし、一般的なエラーを指摘してくれます。
また、サーバーのアクセスログを確認し、AppleのCDN (User-Agentにswcdが含まれる) からAASAファイルへのアクセスが来ているかを確認するのも有効です。
デバイスコンソールの確認
Macに実機を接続し、コンソールアプリ (/Applications/Utilities/Console.app) を使うことで、Universal Linkの検証プロセスに関するOSのログを直接見ることができます。
- コンソールアプリを起動し、接続したiPhoneを選択します。
- 検索バーに
swcd(Associated Domainsを管理するプロセス) と入力します。 - アプリをインストールし直すと、AASAファイルの取得と検証に関するログが表示されます。エラーが出ていないか確認しましょう。
Androidのデバッグ方法
assetlinks.json のバリデーション
Googleが公式のバリデーションツールを提供しています。
-
Statement List Generator and Tester: ドメインとアプリの情報を入力すると、
assetlinks.jsonを生成してくれるだけでなく、サーバーに設置済みのファイルをテストすることもできます。
adbコマンドを使った検証
-
App Link状態の確認: 前述の通り、
adb shell dumpsys package dでドメインの検証状態を確認できます。 -
手動での再検証: アプリを再インストールすることなく、検証プロセスを強制的に実行できます。
adb shell pm verify-app-links --re-verify com.example.myapp -
インテントのテスト: リンクをクリックする代わりに、adbから直接インテントを投げてアプリが起動するかをテストできます。
adb shell am start -a android.intent.action.VIEW \ -c android.intent.category.BROWSABLE \ -d "https://your.domain.com/path"
まとめ
本記事では、Universal LinksとApp Linksの基本的な概念から、それぞれのOSにおける詳細な仕組み、Flutterでの実装方法、そして実践的なトラブルシューティングまでを解説しました。
これらの技術は、単にアプリの特定画面を開くためのショートカットではありません。Webとアプリの境界をなくし、ユーザーに一貫性のあるシームレスな体験を提供するための、プラットフォームが公式にサポートする重要な仕組みです。その根底には、ドメインの所有権を証明することで、安全な関連付けを保証するという共通の思想があります。
設定は複雑で、多くのコンポーネントが正しく連携して初めて機能するため、デバッグは困難を伴います。しかし、その仕組みを一つ一つ理解し、OSが内部で何を行っているのかを把握することで、必ず問題を解決できるはずです。
今後の学習指針
- Firebase Dynamic Links: 今回解説した仕組みの上に、さらに高度な機能(A/Bテスト、OSごとのリダイレクト、インストールされていないユーザーをApp Store/Play Storeに誘導する機能など)を提供するFirebaseのサービスです。本記事の知識は、Dynamic Linksを理解するための基礎となります。
- 各OSの公式ドキュメント: Apple、Googleのドキュメントは常に最新かつ最も正確な情報源です。仕様の変更に追従するためにも、定期的に目を通すことをお勧めします。
Discussion
uni_links は現在 discontinuedかと思いますので、app_links を利用されるのが良いかもしれません。
ご指摘ありがとうございました!
書き換えましたので、またお気づきの点があれば教えていただければと思います。