App Links機能作成に挑戦
モバイルアプリで、以下の流れ、経験したことありませんか?
アプリをインストール
↓
アプリを起動して、フォームからメールアドレスを入力して送信
↓
メールが届く
↓
メール本文のリンクをタップしたら、アプリが起動
↓
登録情報の入力画面が表示される
太字で記載した部分がApp Linksに関係するものです。
あるサイトの画像をお借りしますが、以下の通りです。
URLをタップ時、
対応アプリがインストール済であれば、アプリが起動し、
未インストールであれば、以下の通りアプリインストールに進むか、ブラウザが開くか、そのどちらかになるかと思います。

※App Linksより
ローカル編とリリース編
ボリュームが少し大きめのため、2本立てで進めていました。
既にローカル編のは公開しています。アプリをローカルで動かしながら(USBケーブルでPCと端末を接続して確認)、App Linksの機能構築をする話でした。
今回はリリース編です。
ビルドを行ってapkファイルを作成します。そのapkファイルを端末にインストールする形で動作するか確認する方法です。Play Storeからのインストールとフローは違いますが、本質的には同じで、配布可能な状態となった形のものに対して、動作確認をします。
リリースの定義
Google Play Storeへのリリースは実施していません。AppDistributionにデプロイしたapkファイルを、各端末でダウンロードして利用可能な状態にするところまで実施しています。
「ローカル」に対してどういう単語を用いるか考えた結果「リリース」になりました
こんな方向けの記事です
以下の経験がある方を想定しています。
- Flutterでアプリ開発の経験がある
-
AndroidManifest.xmlの編集経験がある
また、以下のようなトピックに興味がある方はぜひ読んでみてください。参考になれば嬉しいです。
- メールなどのURLからアプリを直接起動させたい
- Firebase Hostingと連携したApp Links実装
- apkファイルでのリリース前動作確認方法
主な作業内容
App Linksを実現するために主な作業は以下の通りです。
-
AndroidManifest.xmlの修正 -
assetlink.jsonのデプロイ(WebURLとAndroidアプリケーションのMapping) - アプリケーションコードで、WebURLを受け取れる状態へ
- アプリ(apkファイル)デプロイと端末へのインストール
アプリケーションの構成
構成としては、大きく2段階です。
この構成実現のために、supabaseとresendを用いています。
1. アプリで会員登録するメールアドレスを入力すると、そのアドレスに仮会員登録完了のメールが届く
2. 届いたメールのURLをタップすると、アプリが起動し、本会員登録情報の入力画面を表示
赤字で記載したURL処理の部分が、App Linksが関係する部分です。
ここから本題に進んでいきます。
AndroidManifest.xmlの修正
ローカル編で実施したことと同じような作業です。
上記記事のディープリンクを受け取るために Android アプリとアプリコードを更新するを参考に記述を追加します。
肝心な部分が2か所あります。
-
android:autoVerify="true"を設定
アプリのインテント フィルタの少なくとも 1 つに android:autoVerify="true" が存在する場合、Android 6.0(API レベル 23)以上を搭載しているデバイスにアプリをインストールすると、システムはアプリのインテント フィルタ内の URL に関連付けられているホストを自動的に検証します。
-
<data>タグ部分
上記2か所の内容について、以下の通り反映します。
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</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="${FIREBASE_HOSTING_DOMAIN}"
android:path="/register"
/>
</intent-filter>
<!-- App Links (開発環境用 - カスタムスキーム、検証なし) -->
ホストの部分にhttpsの情報を追加しました。
私はとりあえず、Firebase Hostingのホストを設定しています。各自値が決まっていると思いますので、その値を設定します。
またドメイン名は、AndroidManifest.xmlには直接記載せず、変数を用いて読み込んでいます。android/secrets.propertiesに、以下のように記載します。
FIREBASE_HOSTING_DOMAIN=xxxxx.web.app
assetlink.jsonのデプロイ(WebURLとAndroidアプリケーションのMapping)
Flutterプロジェクトルートディレクトリのpublicフォルダに資材を追加します。
1. .well-knownフォルダを作成
2..well-knownフォルダにassetlinks.jsonを作成
3. assetlinks.json ファイルを作成してホストする手順を参考に、ファイルの中身を作成
-
delegate_permission/common.handle_all_urls:おまじない的な部分もありますが、大きく以下の役割を担っている- Webサイト(この場合はFirebase Hosting上のサイト)が、指定されたAndroidアプリにURL処理の権限を委譲することを宣言
- 指定されたSHA-256フィンガープリントを持つアプリのみにURLハンドリングを許可
- このサイトのURLがモバイルデバイスで開かれた時、ブラウザではなく直接アプリで開くことを許可
-
namespace:私はAndroidアプリのため、android_appを設定 -
package_name:android/app/build.gradleのnamespaceの値 -
sha256_cert_fingerprints:こちらの記事で実施の通り、keytoolコマンドを用いて、SHA-256の値を取得して設定[ { "relation": [ "delegate_permission/common.handle_all_urls", ], "target": { "namespace": "android_app", "package_name": "com.enoconan.testingappv2", "sha256_cert_fingerprints": [ "43:F2:CF:97:EF:1E:30:3D:86:35:44:69:F8:CF:BD:F9:ED:15:2C:3E:BD:45:F3:34:FA:E0:58:E0:9B:76:FF:D1" ] }, } ]
4. assetlinks.jsonの設定内容検証
Statement List Generator and Testerを用いて検証し、以下画像の通り必要事項を入力し、Test Statementを押下して、Success!となれば正しく設定が完了

5.firebase.jsonの設定
headersキーとrewritesキーの設定値を修正します。
全体実装は、こちらfirebase.jsonで公開しています。
```json
"headers": [
{
"source": "/.well-known/assetlinks.json",
"headers": [
{
"key": "Content-Type",
"value": "application/json"
}
]
}
],
"rewrites": [
{
"source": "**",
"destination": "/index.html"
}
]
```
6. Firebase Hostingへデプロイ
これまでの手順で設定した内容をリモートに反映します。
firebase deploy --only hosting
7.反映内容確認
assetlinks.jsonはブラウザから確認可能
Chromeなどのブラウザで、URLにhttps://{domain}/.well-known/assetlinks.jsonと入力すると、作成したassetlinks.jsonの内容を確認できます。これで正しく反映できているか確認します。
誤って機密情報などを貼り付けたりしないようにしてください。
AndroidManifest.xmlとassetlinks.json
ここまでで、以下2ファイルに関する話をしました。
AndroidManifest.xml-
assetlinks.json
開発時からアプリインストール後までの流れは、アプリリンクの仕組みで、以下の通り記載されています。1は本記事でも記載した通りの話です。
- アプリのマニフェストで、android:autoVerify="true を使用してインテント フィルタ内の URL を宣言し、ウェブサイトのホストを指定します。
- アプリがインストールされると、Android システムはウェブサーバー上の既知の場所から assetlinks.json ファイルを取得します。
- システムは、assetlinks.json ファイルが有効で、sha256_cert_fingerprints がアプリの署名証明書と一致することを確認します。
- ユーザーが一致するリンクをクリックすると、システムは確認ダイアログを表示せずに、ユーザーをアプリに直接誘導します。
アプリケーションコードで、WebURLを受け取れる状態へ
アプリケーションがWeb URLを受け取る場合、デバイス上のアプリケーション状態として、以下の2パターンに分かれます。今回はローカルではないので、どちらも確認が可能です。
- Hot Start(アプリを表示状態、非表示であるが未終了)
- Cold Start(アプリ終了状態)
以下のスニペットでは核となる箇所のみ記載しています。ご了承ください。
全体実装は、こちら(app.dart)で公開しています。
/// アプリケーションのエントリーポイント
class TestingApp extends ConsumerStatefulWidget {
const TestingApp({super.key});
@override
ConsumerState<TestingApp> createState() => _TestingAppState();
}
class _TestingAppState extends ConsumerState<TestingApp> {
late AppLinks _appLinks;
StreamSubscription<Uri>? _linkSubscription;
bool _isProcessingDeepLink = false;
final _router = GoRouter(
initialLocation: MenuPage.routeName,
routes: AppRouter.routes,
redirect: (context, state) {
// リダイレクト処理の詳細ログを追加
if (kDebugMode) {
debugPrint('GoRouter redirect - location: ${state.uri}, path: ${state.uri.path}');
}
return null;
}
);
void openAppLink(Uri uri) {
setState(() {
_isProcessingDeepLink = true;
});
if (!_isValidUri(uri)) {
if (kDebugMode) {
debugPrint('Invalid URI: $uri');
}
_navigateToMenu();
setState(() {
_isProcessingDeepLink = false;
});
return;
}
final path = uri.path.isEmpty ? '/' : uri.path;
if (path == '/register') {
final token = uri.queryParameters['token'] ?? '';
if (token.isEmpty) {
_router.go(SignUpError.routeName);
} else if (_isValidToken(token)) {
_router.go(SignUp.routeName, extra: {'token': token});
} else {
_router.go(SignUpError.routeName);
}
} else {
_navigateToMenu();
}
// 遷移完了後、即座にローディングを解除
setState(() {
_isProcessingDeepLink = false;
});
}
// ディープリンクの初期化と処理
Future<void> initDeepLinks() async {
_appLinks = AppLinks();
// コールドスタート時のディープリンク処理
try {
final uri = await _appLinks.getInitialLink();
if (uri != null) {
openAppLink(uri);
}
} catch (e) {
if (kDebugMode) {
LoggerService.warn('Error getting initial deep link: $e');
}
}
// アプリ起動後のディープリンク処理
_linkSubscription = _appLinks.uriLinkStream.listen((uri) {
openAppLink(uri);
});
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
WidgetsBinding.instance.addPostFrameCallback((_) {
ref.read(responsiveDimensionsProvider.notifier).updateDimensions(size);
});
// ディープリンク処理中はローディング画面を表示
if (_isProcessingDeepLink) {
return const MaterialApp(
home: Scaffold(
body: Center(child: CircularProgressIndicator(color: MyColors.greenButton)),
),
debugShowCheckedModeBanner: false,
);
}
return MaterialApp.router(
routerConfig: _router,
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [Locale('ja', 'JP')],
debugShowCheckedModeBanner: false,
title: 'Testing Sample',
theme: ThemeData(fontFamily: 'NotoSansJP', appBarTheme: const AppBarTheme())
);
}
}
アプリ(apkファイル)デプロイと端末へのインストール
App Distributionを用いて、デプロイ作業を行います。
ここでは具体的なセットアップの話は省いています。
以下記事にて手順は紹介していますので、必要に応じてご覧ください。
また開発時に都度、App Distributionにapkファイルをアップロードするのが面倒になる場合もあるかと思います。
その場合は、実端末とPCをUSBケーブルで接続するUSBデバッグの形をとり、adbコマンドでアプリをインストールすることもできます。
# ビルド
flutter build apk --release
# アンインストール
adb uninstall com.enoconan.testingappv2
# アプリインストール
adb install build/app/outputs/flutter-apk/release.apk
動作確認
以下のように、メールに添付したURLをタップする形で確認します。

大きく3種類の挙動になるパターンで確認してみました。
| パターン | 起動方法 | 表示内容 |
|---|---|---|
| 1.正規パス | アプリ起動 | 会員情報入力画面を表示 |
| 2.パラメーター誤り | アプリ起動 | トークンの有効期限切れ画面表示 |
| 3.パス誤りまたはドメイン名誤り | ブラウザ起動 | 404ページを表示 |
それぞれのパターンに関するスクリーンショットです。想定した通りに制御できていることが確認できました。
| 1.正規パス | 2.パラメーター誤り | 3.パス誤りまたはドメイン名誤り |
|---|---|---|
![]() |
![]() |
![]() |
まとめと今後
前回のローカル編と合わせて、App Linksを実現する作業に取り組んできました。
動作確認の部分で記載の通りですが、きちんと制御できていることが確認できると嬉しいものです。
それぞれ、ざっくりはありますが、以下の内容を理解することができました。
- ローカル編
- App Linksの基本的な内容理解
- adb(Android Debug Bridge)コマンドを用いた動作確認方法
- リリース編:
- コールドスタートでも機能するための実装方法理解
- ドメインに対する検証やパスに関する制御方法
今後ですが、Dynamic App linksに関して触れたいと思っています。
Android 15 以降では、動的アプリリンクが導入され、アプリリンクがさらに強力になります。動的アプリリンクを使用すると、アプリの新しいバージョンを公開することなく、サーバーサイドの assetlinks.json ファイルでディープリンク ルールを更新できます。
URL パス、フラグメント、クエリ パラメータのマッチングなど、アプリを開く URL をきめ細かく制御することもできます。
AndroidManifest.xmlの修正のdataタブで、URLのschema,host,pathに関するルールを記述していました。
App Linksの挙動を変えたい場合、以下のどちらかになります。
AndroidManifest.xml- アプリケーションのコード修正
どちらにせよ再度apkファイルをビルドするので再リリースが必要です。公開済のアプリであれば、再インストールをお願いすることになります。
これを避けるために、assetlinks.json側でパスやクエリに関する条件を定義しよう!となっています。
動的アプリリンクで記載された以下内容が該当箇所です。
システムは assetlinks.json ファイルを定期的に再取得して最新のルールを取得するため、アプリをアップデートしなくてもリンクを更新できます。定期的な再取得は、Google サービスがインストールされている Android 15(API レベル 35)以降を搭載したデバイスでサポートされています。
Android 15(API レベル 35)に関する話は、以下が参考になります。
参考記事




