Flutter DeepLinkの使い方と設定手順を分かりやすく解説

はじめに

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のみ使用します:

  1. Firebase Consoleで新しいプロジェクトを作成
  2. Firebase Hostingを有効化してドメインを取得
  3. 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_enabledtrueにすると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が動作しないケースが多いため、ファイル配置は入念に確認してください
  • シミュレーターでは挙動が不安定な場合があるため、可能な限り実機で確認することを推奨します

参考

株式会社SPで一緒に働いてみませんか?

SPはエンジニアの成長を大切にする会社です。

ご興味ある方は一度気軽な雰囲気で、カジュアル面談はいかがでしょうか?

どのような課題を
解決したいですか?

株式会社SPでは、お客様の取り組みに寄り添いながら、
課題解決を伴走支援していきます。

まずはお気軽にこちらからお問い合わせください。

お問い合わせ・相談する(無料)