0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Flutter】Android App Links(Deep Link)でWebURLからアプリを起動してみる(ローカル編)

Last updated at Posted at 2025-05-04

DeepLink機能作成に挑戦

モバイルアプリで、以下の流れを経験したこと、ありませんか?

アプリをインストール

アプリを起動して、フォームからメールアドレスを入力して送信

メールが届く

メール本文のリンクをタップしたら、アプリが起動

登録情報の入力画面が表示される

太字で記載した部分がDeepLinkに関係するものです。

今回はこの機能を実現してみたいと思います。
また私自身の端末がAndroid、ということもあり、iOS関連に関する作業は行っていません。

ローカル編とリリース編

ボリュームが少し大きめのため、2本立てで進めます。
まずはローカル編です。
ローカルで起動したアプリケーションを使って、機能するように作業を進めます。

リリース編では、デプロイしたアプリケーション(apkファイル)を端末にインストールして起動したアプリで機能させる作業になります。

(誤解を招くかもしれないので記載しますが、Google Play Storeへのリリースは実施していません。ローカルに対してどういう単語を用いるか考えた結果、リリース、になりました)

作業の前提

以下ご経験のある方が進めやすい内容だと思います。

  • Flutterのアプリ作成とFirebaseの利用(個人開発レベルでOK!)
  • AndroidManifest.xmlファイルを修正

主な作業内容

ローカル環境でDeepLinkを実現するために主な作業は以下の通りです。

  • Firebase Hostingのセットアップ(作業量の調整の関係でこちらで実施)
  • AndroidManifest.xmlの修正
  • アプリケーションのコードで、WebURLを受け取れる状態を作成

Firebase Hostingのセットアップ

ローカルで動かすだけの場合、ここは必要ありません。
後編(リリース編)の内容とのバランスの関係で、プロジェクト作成をこのタイミングで実施します。

詳細は後編で実施しますが、アプリケーションがhttpsのWebURLを受け付けられるよう、Firebase Hostingにデプロイします。

また、アプリケーションはAndroidデバイス上で動かすものなので、資材をビルドしてそれをデプロイするというよりは、
WebURLとAndroidアプリケーションのMappingで必要な情報をデプロイする上で必要になります。

では進めていきます。
Firebaseでプロジェクトを作成の上、Hostingの画面を開きます。
「始める」とあるので、クリックします。
image.png

Firebase CLI のインストール

コマンドをコピーして、インストールします。
image.png

プロジェクトの初期化

こちらもコマンドを実行します。
image.png

firebase initを実行すると、何点か聞かれるので回答します。
image.png

どのプロジェクトを利用するか選択します。
image.png

Hostingの設定を行います。
最初のpublicですが、何も入力せずに、Enterを押せば、勝手にpublicが設定されます。
残り2項目はNを入力して進みます。
image.png

ここまで進めるとpublicフォルダ生成されています。
このpublicフォルダはリリース編で動作確認する際に必要です。

AndroidManifest.xmlの修正

上記記事のディープリンクを受け取るために Android アプリとアプリコードを更新するを参考に記述を追加します。

以下内容の追記が必要です。リンクに記載されたコードとほぼ同じです。

<activity
    android:name=".MainActivity"
    android:exported="true">
    <!-- 略 -->
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
    <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" />
        <!-- local  -->
        <data
            android:scheme="http"
            android:host="local.com"
            android:pathPrefix="/register"
        />
    </intent-filter>

intent-filterが2箇所あります。

  • 1つ目
    DeepLink特有の話ではありませんが、端末上でHome画面(アプリ一覧)にアプリを表示する上で必要な記述です。
    この記述がない場合、Home画面からアプリをタップして起動する、という当たり前のようにやっている方法でアプリ起動が出来なくなってしまいます。
  • 2つ目
    こちらがDeepLinkに関係する内容です。
    • android:autoVerify="true":リンク先の文章にもある通りです。

      また、android:autoVerify="true" 属性も指定する必要があります。これにより、アプリをこの特定の種類のリンクのデフォルト ハンドラに指定できます。

    • android.intent.category.BROWSABLE
      • 英単語から推測できますが、ブラウザから起動可能な状態にします。
    • dataタグ
      • ここもリンク先の文章の通りです。私は値を少々修正しているので、その内容で一部上書きすると、以下の通りです。

        このステップでは、local ドメインからのディープリンクの処理先として /register パス プレフィックスを含む MainActivity を指定します。

アプリケーション側で、WebURLを受け取れる状態へ

アプリケーションがWeb URLを受け取るとき、
デバイス上のアプリケーション状態として、大きく2パターンに分かれます。

  • Cold Start:アプリを終了した状態
  • Hot Start / Warm Start:Cold Start以外の状態(※)

詳細は以下に記載されています。

ローカル環境は、Cold Startの動作確認はできません。
(アプリが終了した時点で、接続が切れてしまう(Lost Connection状態になる)ため)
ローカルの動作確認では、Hot Start / Warm Startについて、想定する挙動になることを確認します。

Flutterのlib/main.dartでrunAppに設定しているWidgetに対して、WebURLのハンドリングを追加します。(※)

※以下の例でいえば、TestingAppウィジェットのことです。

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        body: Center(
          child: Text('Hello, world!'),
        ),
      ),
    );
  }
}

実装する前に、以下パッケージを追加します。

アプリケーション起動時に以下の処理を行っています。(一部略)
先ほどの例で記載したMyAppが、私の場合はTestingAppになっています。

全体実装は、こちら(app_local.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;

  final _router = GoRouter(
    initialLocation: MenuPage.routeName,
    routes: Routes.routeList,
    redirect: (context, state) {
      return null;
    },
  );

  @override
  void initState() {
    super.initState();
    initDeepLinks();
  }

  @override
  void dispose() {
    _linkSubscription?.cancel();
    super.dispose();
  }

  void openAppLink(Uri uri) {
    final path = uri.path.isEmpty ? '/' : uri.path;
    if (kDebugMode) {
      print('Opening deep link path: $path');
    }

    // 会員登録画面へ(トークン情報を渡す)
    if (path == '/register') {
      final token = uri.queryParameters['token'] ?? '';
      if (token.isEmpty) {
        if (kDebugMode) {
          print('Register Token is empty');
        }
        _router.go(SignUpFailed.routeName);
      } else {
        _router.go(SignUp.routeName, extra: {'token': token});
      }
    } else {
      _router.go('/');
    }
  }

  // ディープリンクの初期化と処理
  Future<void> initDeepLinks() async {
    _appLinks = AppLinks();

    // アプリ起動後のディープリンク処理
    _linkSubscription = _appLinks.uriLinkStream.listen((uri) {
      if (kDebugMode) {
        print('Got deep link while app running: $uri');
      }
      openAppLink(uri);
    });
  }
}

_appLinks.uriLinkStream.listenを呼び出し、WebURLを受け付け状態を整えます。
openAppLinkメソッドには、受け取ったURLに応じた遷移先を決める処理を記述しています。

context.go('/')はエラーになった

実装に関する気付きみたいな内容なので、気になる方は読んでください。
私が作成しているアプリケーションはGo Routerによる画面遷移を管理しています。

どの画面に遷移するか、最初は以下の実装をしていました。

context.go('/');

GoRouterを使った画面遷移は、context.gocontext.pushを用いることが多いです。

  • context.go:これまでのスタック(※)を削除して、新しいスタックを作成し、そこにウィジェットを載せる
  • context.push:これまでのスタック(※)に遷移先のウィジェットを重ねる
    ※スタック:スタック/キューのスタックのことですが、それぞれ以下の違いがあります。

アプリ側でリンクを受け取るのは成功しましたが、
実際に画面遷移するための処理で以下エラーが発生しました。(アプリケーションの構造によっては、発生しないエラーかもしれません)

I/flutter (21437): /register
E/flutter (21437): [ERROR:flutter/runtime/dart_vm_initializer.cc(40)] Unhandled Exception: 'package:go_router/src/router.dart': Failed assertion: line 521 pos 12: 'inherited != null': No GoRouter found in context

まだMaterialApp.routerにルーティングに関する定義をしていない状態で、GoRouterを用いた遷移処理を実行しようとしたからです。
ざっくり記載すると、UI制御でGoRouterを使うための初期設定が完了していないのに、遷移する処理を呼び出しているよ、といったエラーです。

エラー回避するため、
GoRouterインスタンスを定義した変数を直接利用して、遷移処理を実装しました。

_router.go('/');

動作確認

adbコマンドを使って、挙動を確認します。

指定されたAndroidアプリ(com.sample.package)を起動し、特定のURL(http://local.com/register?token=ABCDE)をそのアプリで開くよう指示しています。

adb shell am start -W -a android.intent.action.VIEW -d "http://local.com/register?token=ABCDE" com.sample.package

アプリが起動して、
実装したハンドリング通りの画面が表示できていれば成功です。

まとめと今後

Android App Links(Deep Link)の機能実装について、まずはローカルで起動したアプリに適用する方法について説明してきました。
この後、リリース編でデプロイしたapkファイルでも機能するように作業を進めていきます。

予定作業は以下の通りです。

  • WebURLとAndroidアプリケーションをMapping
  • AndroidManifest.xmlの修正
  • App Distributionを用いたアプリケーションのデプロイ

参考記事

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?