🎮

AdMob のインタースティシャル広告を Riverpod で管理する方法

2025/02/26に公開

概要

インタースティシャル広告は、アプリの自然な切れ目(レベルクリア後やコンテンツの切り替わり時など)に表示される全画面広告です。
YoTube や無料ゲームアプリなどのコンテンツの途中で出てくるような全画面の広告です。
インタースティシャル(interstitial)ってスペルも難しいしわかりにくいのでアプリ内や脳内では全画面広告と言っています。

この記事では全画面広告の状態を Riverpod で管理する方法について紹介しています

前提条件

  • Flutter プロジェクトがすでに設定されていること
  • AdMob アカウントがあり、インタースティシャル広告の広告ユニット ID を取得済みであること
  • 必要なパッケージのインストール:
flutter pub add google_mobile_ads
flutter pub add flutter_riverpod

実装手順

1. InterstitialAdState クラスと StateNotifier の定義

インタースティシャル広告の状態を管理するクラスを作成します:

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

enum InterstitialAdStatus { initial, loading, loaded, failed, shown }

class InterstitialAdState {
  final InterstitialAdStatus status;
  final InterstitialAd? ad;
  final String? errorMessage;

  InterstitialAdState({
    this.status = InterstitialAdStatus.initial,
    this.ad,
    this.errorMessage,
  });

  InterstitialAdState copyWith({
    InterstitialAdStatus? status,
    InterstitialAd? ad,
    String? errorMessage,
  }) {
    return InterstitialAdState(
      status: status ?? this.status,
      ad: ad ?? this.ad,
      errorMessage: errorMessage ?? this.errorMessage,
    );
  }

  bool get isAdReady => status == InterstitialAdStatus.loaded && ad != null;
}

class InterstitialAdNotifier extends StateNotifier<InterstitialAdState> {
  InterstitialAdNotifier() : super(InterstitialAdState());

  Future<void> loadAd(String adUnitId) async {
    // 既に読み込み中または読み込み済みの場合は何もしない
    if (state.status == InterstitialAdStatus.loading ||
        state.status == InterstitialAdStatus.loaded) {
      return;
    }

    state = state.copyWith(status: InterstitialAdStatus.loading);

    try {
      await InterstitialAd.load(
        adUnitId: adUnitId,
        request: const AdRequest(),
        adLoadCallback: InterstitialAdLoadCallback(
          onAdLoaded: (InterstitialAd ad) {
            // 広告の読み込みが完了したら、フルスクリーン表示時のコールバックを設定
            ad.fullScreenContentCallback = FullScreenContentCallback(
              onAdDismissedFullScreenContent: (ad) {
                // 広告が閉じられたら破棄して新しい広告を読み込む
                ad.dispose();
                state = state.copyWith(
                  status: InterstitialAdStatus.initial,
                  ad: null,
                );
                loadAd(adUnitId); // 次の広告をプリロード
              },
              onAdFailedToShowFullScreenContent: (ad, error) {
                ad.dispose();
                state = state.copyWith(
                  status: InterstitialAdStatus.failed,
                  errorMessage: 'Failed to show ad: ${error.message}',
                  ad: null,
                );
              },
            );

            state = state.copyWith(
              status: InterstitialAdStatus.loaded,
              ad: ad,
            );
          },
          onAdFailedToLoad: (LoadAdError error) {
            state = state.copyWith(
              status: InterstitialAdStatus.failed,
              errorMessage: 'Failed to load ad: ${error.message}',
            );
          },
        ),
      );
    } catch (e) {
      state = state.copyWith(
        status: InterstitialAdStatus.failed,
        errorMessage: e.toString(),
      );
    }
  }

  Future<bool> showAdIfReady() async {
    if (state.isAdReady) {
      try {
        await state.ad!.show();
        state = state.copyWith(status: InterstitialAdStatus.shown);
        return true;
      } catch (e) {
        state = state.copyWith(
          status: InterstitialAdStatus.failed,
          errorMessage: 'Error showing ad: $e',
        );
        return false;
      }
    }
    return false;
  }

  void disposeAd() {
    if (state.ad != null) {
      state.ad!.dispose();
      state = state.copyWith(
        status: InterstitialAdStatus.initial,
        ad: null,
      );
    }
  }
}

final interstitialAdProvider = StateNotifierProvider<InterstitialAdNotifier, InterstitialAdState>((ref) {
  return InterstitialAdNotifier();
});

2. インタースティシャル広告を管理するサービスクラス

広告の読み込みと表示を管理するサービスクラスを作成します:

import 'package:flutter/foundation.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class AdService {
  final Ref ref;

  AdService(this.ref);

  // アプリ起動時に広告を事前に読み込む
  void preloadInterstitialAd() {
    ref.read(interstitialAdProvider.notifier).loadAd(_getInterstitialAdUnitId());
  }

  // 広告を表示する
  Future<bool> showInterstitialAd() async {
    final adNotifier = ref.read(interstitialAdProvider.notifier);
    final adState = ref.read(interstitialAdProvider);

    // 広告がまだ読み込まれていない場合は読み込む
    if (!adState.isAdReady) {
      await adNotifier.loadAd(_getInterstitialAdUnitId());
      // 読み込みが完了するまで少し待つ
      await Future.delayed(const Duration(seconds: 1));
    }

    // 広告を表示
    return await adNotifier.showAdIfReady();
  }

  // テスト用と本番用の広告ユニットIDを切り替える
  String _getInterstitialAdUnitId() {
    // デバッグモードではテスト広告を表示
    if (kDebugMode) {
      return 'ca-app-pub-3940256099942544/1033173712'; // テスト用ID
    } else {
      return 'YOUR_PRODUCTION_INTERSTITIAL_AD_UNIT_ID'; // 本番用ID
    }
  }
}

final adServiceProvider = Provider<AdService>((ref) {
  return AdService(ref);
});

3. アプリへの統合

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';

void main() {
  // AdMob の初期化
  WidgetsFlutterBinding.ensureInitialized();
  MobileAds.instance.initialize();

  runApp(
    const ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends ConsumerWidget {
  const MyApp({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    // アプリ起動時に広告をプリロード
    WidgetsBinding.instance.addPostFrameCallback((_) {
      ref.read(adServiceProvider).preloadInterstitialAd();
    });

    return MaterialApp(
      title: 'インタースティシャル広告デモ',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: const GameScreen(),
    );
  }
}

class GameScreen extends ConsumerWidget {
  const GameScreen({Key? key}) : super(key: key);

  
  Widget build(BuildContext context, WidgetRef ref) {
    // 広告の状態を監視
    final adState = ref.watch(interstitialAdProvider);

    return Scaffold(
      appBar: AppBar(
        title: const Text('ゲーム画面'),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'ゲームコンテンツ',
              style: Theme.of(context).textTheme.headlineMedium,
            ),
            const SizedBox(height: 20),
            Text(
              '広告の状態: ${adState.status.toString()}',
              style: Theme.of(context).textTheme.bodyLarge,
            ),
            const SizedBox(height: 40),
            ElevatedButton(
              onPressed: () async {
                // ゲームレベルクリア時などに広告を表示
                final adShown = await ref.read(adServiceProvider).showInterstitialAd();
                if (adShown) {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('広告が表示されました')),
                  );
                } else {
                  ScaffoldMessenger.of(context).showSnackBar(
                    const SnackBar(content: Text('広告の準備ができていません')),
                  );
                  // 広告が表示できなかった場合は再読み込み
                  ref.read(adServiceProvider).preloadInterstitialAd();
                }
              },
              child: const Text('レベルクリア(広告表示)'),
            ),
          ],
        ),
      ),
    );
  }
}

おまけ: インタースティシャル広告の表示タイミング

全画面広告の一般的な表示タイミング:

  1. ゲームのレベルクリア後
  2. アプリの画面遷移時
  3. 記事や動画の閲覧完了後
  4. アプリの一時停止時
  5. コンテンツの合間(YouTube や Tver など)

アンチパターン

  1. アプリ起動直後
  2. ユーザーが何かのタスクの途中
  3. 頻繁すぎる表示

1 のように起動したときに広告を表示したい場合は起動時広告(Open Ad)を検討するのが良いでしょう。
2 は広告停止されてしまう可能性もあり最も気をつけるべきです
3 も同様に停止されるおそれがありますし、体験がとにかく悪いのでやめたほうが良さそうです。

まとめ

Riverpod を使用してインタースティシャル広告を管理することで、広告の読み込みと表示のタイミングを効率的に制御できます!適切なタイミングで広告を表示することで、ユーザー体験を損なわずに収益化を実現しましょう。

参考資料


宣伝

実際に AdMob を駆使してマネタイズする具体的な手法について、有料本ですが月 10 万を超える!個人開発 ×AdMob 攻略ガイドでも書いてます!よかったら御覧ください

Discussion