实用经验 アプリ開発 読了目安 約7分

FlutterでATT・同意フォーム・AdMobを組み込む順番|ポップアップを重ねない実装

広告付きのFlutterアプリを作ると、起動直後に「トラッキングの許可」「プライバシーの同意」「通知の許可」と確認画面が立て続けに出てしまいがちです。審査でも利用者の印象でも損をしないよう、呼び出す順番を整理した実装例を紹介します。

起動時に出る3つの確認画面

AdMobで広告を表示するFlutterアプリでは、起動時におおむね次の3つの処理が関係します。

処理対象役割
同意フォーム(UMP)主にEEA・英国などの利用者広告のための個人データ利用について同意を得る
ATT(App Tracking Transparency)iOS 14.5以降他社のアプリやサイトをまたいだ追跡の許可を得る
AdMobの初期化と広告の読み込みすべての利用者広告SDKを準備して広告を取得する

問題は、これらを別々の場所で好き勝手に呼ぶと、ダイアログが重なって表示されたり、片方が出なかったりすることです。特にATTのダイアログは、アプリが完全に前面に表示されていない状態で呼ぶと表示されないことがあります。App Storeの審査では「ATTのダイアログが確認できない」という理由で差し戻される例もあるため、確実に出る順番で呼ぶことが大切です。

おすすめの順番

  1. 最初の画面を表示し、描画が終わるのを待つ。
  2. UMPで同意情報を更新し、必要な地域の利用者にだけ同意フォームを表示する。
  3. 同意フォームが閉じたあとで、iOSならATTの状態を確認し、未決定のときだけダイアログを出す。
  4. 広告をリクエストしてよい状態であれば、AdMobを初期化して広告を読み込む。
  5. 通知の許可など、アプリ固有の確認は、その機能を使う場面まで後回しにする。

Googleのドキュメントでも、広告をリクエストする前に同意を取得することが求められています。そして、ダイアログは必ず1つずつ、前のものが閉じてから次を出す。この2点を守れば、重なりの問題はほぼ起きません。

事前に必要な設定

pubspec.yaml

dependencies:
  google_mobile_ads: ^x.y.z        # pub.dev の最新安定版を指定
  app_tracking_transparency: ^x.y.z

バージョンは pub.dev で最新の安定版を確認して指定します。同意フォーム(UMP)の機能は google_mobile_ads に含まれているため、別のパッケージは不要です。追加後は flutter pub get を実行し、iOSでは ios フォルダで pod install が通ることを確認します。

iOS:Info.plist

<key>GADApplicationIdentifier</key>
<string>ca-app-pub-3940256099942544~1458002511</string>
<key>NSUserTrackingUsageDescription</key>
<string>お客様に合った広告を表示するために使用します。</string>

上のアプリIDはGoogleが公開しているテスト用です。公開前に自分のアプリIDへ差し替えます。NSUserTrackingUsageDescription の文言はダイアログにそのまま表示されるため、何のために使うのかを具体的に書きます。この項目がないままATTを呼ぶと、アプリがクラッシュする原因になります。AdMobのドキュメントに記載されている SKAdNetworkItems の追加も忘れずに行います。

Android:AndroidManifest.xml

<meta-data
    android:name="com.google.android.gms.ads.APPLICATION_ID"
    android:value="ca-app-pub-3940256099942544~3347511713"/>

こちらもテスト用のアプリIDです。application タグの中に記述します。

実装例

起動処理を1つのクラスにまとめ、同時に2回呼ばれても処理が重複しないようにしています。

import 'dart:async';
import 'dart:io';

import 'package:app_tracking_transparency/app_tracking_transparency.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';

class AdsBootstrap {
  AdsBootstrap._();
  static final AdsBootstrap instance = AdsBootstrap._();

  Future<bool>? _running;

  /// 同意 → ATT → 初期化 の順で1回だけ実行する
  Future<bool> start() => _running ??= _run();

  Future<bool> _run() async {
    await _gatherConsent();
    await _requestTrackingIfNeeded();

    final canRequest = await ConsentInformation.instance.canRequestAds();
    if (!canRequest) return false;

    await MobileAds.instance.initialize();
    return true;
  }

  Future<void> _gatherConsent() {
    final completer = Completer<void>();
    ConsentInformation.instance.requestConsentInfoUpdate(
      ConsentRequestParameters(),
      () {
        ConsentForm.loadAndShowConsentFormIfRequired((FormError? error) {
          if (!completer.isCompleted) completer.complete();
        });
      },
      (FormError error) {
        if (!completer.isCompleted) completer.complete();
      },
    );
    return completer.future;
  }

  Future<void> _requestTrackingIfNeeded() async {
    if (!Platform.isIOS) return;
    final status =
        await AppTrackingTransparency.trackingAuthorizationStatus;
    if (status == TrackingStatus.notDetermined) {
      // 直前のダイアログが閉じきるのを少し待つ
      await Future<void>.delayed(const Duration(milliseconds: 400));
      await AppTrackingTransparency.requestTrackingAuthorization();
    }
  }
}

呼び出しは、最初の画面が表示されたあとに行います。

@override
void initState() {
  super.initState();
  WidgetsBinding.instance.addPostFrameCallback((_) async {
    final ready = await AdsBootstrap.instance.start();
    if (!mounted || !ready) return;
    _loadBanner(); // テスト用ユニットIDで広告を読み込む
  });
}

バナー広告のテスト用ユニットIDは、Androidが ca-app-pub-3940256099942544/6300978111、iOSが ca-app-pub-3940256099942544/2934735716 です。開発中に本番のIDで広告をクリックすると、アカウントに悪影響が出るおそれがあるため、必ずテスト用IDを使います。

実装で気をつけたいこと

  • ローディング表示:同意フォームの読み込みには通信が必要です。最初の画面にはコンテンツを先に出し、広告の枠だけを後から表示する作りにすると、待たされている印象がありません。
  • UMPでATTの説明画面を使う場合:AdMobの管理画面でIDFAの説明メッセージを設定すると、同意フォームの流れの中でATTダイアログが表示されます。その場合は自前のATT呼び出しは状態確認だけになりますが、上のコードは「未決定のときだけ出す」ため二重には表示されません。
  • 拒否されても広告は出る:ATTを許可されなくても、パーソナライズされない広告は配信されます。拒否されたからといって機能を制限するのは、Appleのガイドラインで禁じられているので避けます。
  • タスクキルへの備え:全画面広告の表示中にアプリが終了されると、次回起動時に広告の状態は残りません。広告オブジェクトは画面の破棄時に dispose() し、「広告を見たら報酬」などの処理は広告の完了コールバックで確定させるようにします。

審査前のチェックリスト

  1. アプリを削除して入れ直し、初回起動でATTダイアログが確実に表示されるか確認する。
  2. ATTを「許可しない」にしても、アプリの機能が普通に使えるか確認する。
  3. 設定アプリでトラッキングをオフにした状態でもクラッシュしないか確認する。
  4. テスト用IDが本番用IDに差し替わっているか、逆に開発中は本番IDを使っていないかを確認する。
  5. App Store Connectのプライバシー情報で、広告に関するデータの扱いを申告しているか確認する。

確認画面は、アプリと利用者が最初に交わす会話のようなものです。いきなり何度も許可を求めるより、画面を見せてから1つずつ丁寧に聞く方が、許可率も印象も良くなります。