はじめに
DeepLink(ディープリンク)を使うと、WebサイトやメールのリンクからアプリのURLを直接開けるようになります。
この記事では、FlutterでDeepLinkを実装する方法をまとめました。Android・iOS両対応です。
今回作るもの
Firebase HostingのHTTPSドメインで、Universal Links(iOS)とApp Links(Android)を動かします。
https://your-app.web.app/profile?id=123 をタップ → アプリ起動 → プロフィール画面表示、という流れです。
背景
Universal Links(iOS)とApp Links(Android)は、AppleとGoogleがそれぞれ公式に提供しているディープリンクの仕組みで、以下の理由から採用しました。
- 公式のベストプラクティス: 各プラットフォームが推奨する標準的な実装方法
- 代替手段との比較: カスタムURLスキーム(
myapp://)は他のアプリに乗っ取られるリスクがあるため非推奨
前提
- Flutterでアプリ開発の経験があり、基本的なWidget構成やパッケージ追加ができること
flutter createでプロジェクト作成からflutter runでの実行までの一連の流れを理解していること- Android Studio または VS Code が導入済みであること
- エミュレーターまたは実機がある
- Apple Developer Programに登録済み(iOSで必須)
検証環境
- macOS Sonoma 14.1.1
- Android Studio Hedgehog 2023.1.1 Patch 1
- Flutter 3.22.2 / Dart 3.4.3
事前準備
Flutterプロジェクトの作成
新規プロジェクトを作成します。
flutter create deeplink_demo
Firebaseプロジェクトの準備
以下の手順でFirebaseの準備を完了させてください。この記事ではFirebase Hostingのみ使用します:
- Firebase Consoleで新しいプロジェクトを作成
- Firebase Hostingを有効化してドメインを取得
- Hostingが有効化されており、デプロイ可能な状態になっていることを確認
Firebase Hostingの設定
Firebaseプロジェクトの作成
Firebase Consoleで新しいプロジェクトを作成し、Hostingを有効化します。Hostingのセットアップが完了し、デプロイ可能な状態になっていれば準備完了です。
CLIでデプロイ
Firebase CLIを使用してデプロイを行います。事前にNode.jsのインストールが必要です。
# Firebase CLIのインストール
npm install -g firebase-tools
# Firebaseへログイン
firebase login
# プロジェクトの初期化
firebase init hosting
# デプロイ
firebase deploy --only hosting
デプロイが完了すると、https://your-app.web.app形式のURLが表示されます。このURLを控えておいてください。
設定ファイルの配置
iOS用 → public/.well-known/apple-app-site-association
{
"applinks": {
"apps": [],
"details": [
{
"appID": "TEAM_ID.BUNDLE_ID",
"paths": ["*"]
}
]
}
}
TEAM_IDはApple Developer ProgramのチームID、BUNDLE_IDはアプリのBundle Identifierに置き換えてください。
注意:
"paths": ["*"]はすべてのパスを許可する設定です。本番環境では、セキュリティ上、必要なパスのみに制限することを推奨(例:"paths": ["/profile/*", "/settings"])。
Android用 → public/.well-known/assetlinks.json
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target": {
"namespace": "android_app",
"package_name": "com.example.your_app",
"sha256_cert_fingerprints": ["YOUR_APP_FINGERPRINT"]
}
}
]
package_nameはアプリのパッケージ名、sha256_cert_fingerprintsは署名証明書のフィンガープリントに置き換えてください。
フィンガープリントは以下のコマンドで取得できます(開発環境ではdebug.keystoreを使用):
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
注意: 本番リリース時は、リリース用のkeystoreから取得したフィンガープリントを使用してください。
プラットフォーム別の設定
Android(App Links)
android/app/src/main/AndroidManifest.xmlの<activity>タグに以下を追加します。
<activity
android:name=".MainActivity"
android:exported="true"
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>
<!-- Deep Link有効化のメタデータ -->
<meta-data
android:name="flutter_deeplinking_enabled"
android:value="true" />
<!-- 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-app.web.app" />
</intent-filter>
</activity>
ポイント:
flutter_deeplinking_enabledをtrueにするとFlutter標準のDeepLink機能が有効になりますyour-app.web.appは自分のFirebase Hostingドメインに置き換えてください
iOS(Universal Links)
ios/Runner/Info.plistに以下を追加します。
<dict>
<!-- FlutterのDeepLink機能を有効化 -->
<key>FlutterDeepLinkingEnabled</key>
<true/>
<!-- Associated Domainsの設定 -->
<key>com.apple.developer.associated-domains</key>
<array>
<string>applinks:your-app.web.app</string>
</array>
</dict>
Xcodeでも設定が必要です。ios/Runner.xcworkspaceを開いて、Runner → Signing & Capabilities → + Capability → Associated Domains を追加してください。Domainsにapplinks:your-app.web.appを入力します。
※ドメインは自分のものに置き換えてください。Apple Developer Programの登録とチームID設定は必須です。
Flutter側の実装
パッケージ追加
app_linksを使用します。このパッケージを選定した理由は以下の通りです:
- iOS・Android両プラットフォームに対応しており、コードの統一が可能
- Flutter公式ドキュメントでも推奨されているパッケージ
flutter pub add app_links
flutter clean && flutter pub get
コード実装
lib/main.dartにリスナーを実装していきます。
import 'package:flutter/material.dart';
import 'package:app_links/app_links.dart';
import 'dart:async';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'DeepLink Demo',
theme: ThemeData(primarySwatch: Colors.blue),
home: MyHomePage(),
);
}
}
class MyHomePage extends StatefulWidget {
@override
State<MyHomePage> createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
StreamSubscription? _linkSubscription;
String _receivedLink = '待機中...';
String? _parameter;
@override
void initState() {
super.initState();
_initDeepLinkListener();
}
Future<void> _initDeepLinkListener() async {
final appLinks = AppLinks();
// リンクストリームを監視
_linkSubscription = appLinks.uriLinkStream.listen(
(Uri uri) {
print('DeepLinkを受信: $uri');
if (uri.scheme == 'https' && uri.host == 'your-app.web.app') {
_handleDeepLink(uri);
}
},
onError: (err) {
print('リンク受信エラー: $err');
},
);
}
void _handleDeepLink(Uri uri) {
setState(() {
_receivedLink = uri.toString();
_parameter = uri.queryParameters['id'];
});
// パスに応じた処理を実行
if (uri.path == '/profile' && _parameter != null) {
_navigateToProfile(_parameter!);
} else if (uri.path == '/settings') {
_navigateToSettings();
}
}
void _navigateToProfile(String userId) {
// プロフィール画面への遷移処理
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProfilePage(userId: userId),
),
);
}
void _navigateToSettings() {
// 設定画面への遷移処理
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => SettingsPage(),
),
);
}
@override
void dispose() {
_linkSubscription?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('DeepLink Demo')),
body: Padding(
padding: EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'受信したリンク:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
SizedBox(height: 16),
Container(
padding: EdgeInsets.all(12),
width: double.infinity,
decoration: BoxDecoration(
color: Colors.grey[200],
borderRadius: BorderRadius.circular(8),
),
child: Text(_receivedLink, style: TextStyle(fontSize: 16)),
),
if (_parameter != null) ...[
SizedBox(height: 16),
Text(
'パラメータ: $_parameter',
style: TextStyle(fontSize: 16, color: Colors.blue),
),
],
],
),
),
);
}
}
// ダミーのプロフィール画面
class ProfilePage extends StatelessWidget {
final String userId;
const ProfilePage({Key? key, required this.userId}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('プロフィール')),
body: Center(
child: Text('ユーザーID: $userId', style: TextStyle(fontSize: 24)),
),
);
}
}
// ダミーの設定画面
class SettingsPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('設定')),
body: Center(
child: Text('設定画面', style: TextStyle(fontSize: 24)),
),
);
}
}
テスト
Androidエミュレーター
adbコマンドで直接URLを実行します。
adb shell am start -a android.intent.action.VIEW -d "https://your-app.web.app/profile?id=123"
iOSシミュレーター
xcrunコマンドで開きます。
xcrun simctl openurl booted "https://your-app.web.app/profile?id=123"
実機
Android → ChromeでURLにアクセスして「アプリで開く」が表示されれば想定通りの挙動です。
iOS → Safariでアクセスしてアプリがシームレスに起動すれば想定通りの挙動です。
動かないとき
以下の点を確認してください:
- AndroidManifest.xml / Info.plist の設定に誤りがないか確認
- Firebase Hostingの
.well-known配下にファイルが正しく配置されているか確認 - アプリを完全に終了してから再テスト
flutter clean && flutter pub getを実行してリビルド
動作確認
正しく実装できていれば、URLアクセスでアプリが起動し、リンクとパラメータが画面に表示されます。
まとめ
FlutterでDeepLink実装する方法を紹介しました。
実装した内容:
- Firebase HostingでHTTPSドメインを用意
- Android/iOS両方に対応した設定
app_linksパッケージでリンク受け取り- パスに応じた画面遷移
DeepLinkは、SNS連携やプッシュ通知からの遷移など、さまざまな場面で活用できます。
ハマりやすいポイント
.well-knownのパス設定ミスでDeepLinkが動作しないケースが多いため、ファイル配置は入念に確認してください- シミュレーターでは挙動が不安定な場合があるため、可能な限り実機で確認することを推奨します