.NET MAUI で Google AdMob 広告を表示する完全ガイド

.NET 10 + Plugin.MauiMTAdmob v2.4.0 の実装例です。 バナー広告とリワード広告の両方をカバーします。


目次

  1. はじめに
  2. 環境・前提条件
  3. セットアップ手順
  4. バナー広告の実装
  5. リワード広告の実装
  6. ViewModel での使い方
  7. ハマりポイントと解決策
  8. まとめ

はじめに

.NET MAUI アプリに Google AdMob 広告を組み込むのは、一見シンプルに見えて意外とハマりどころが多いです。 本記事では、実際の開発で遭遇したトラブルとその解決策を含め、バナー広告・リワード広告の実装方法を解説します。


環境・前提条件

項目 バージョン
.NET 10
.NET MAUI 最新
Plugin.MauiMTAdmob 2.4.0
CommunityToolkit.Mvvm 8.4.2
ターゲット Android(iOS も対応可)

セットアップ手順

Step 1: NuGet パッケージのインストール

<PackageReference Include="Plugin.MauiMTAdmob" Version="2.4.0" />

Step 2: AndroidManifest.xml に AdMob App ID を追加

Platforms/Android/AndroidManifest.xml に以下を追加します。

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
  <application android:allowBackup="true"
               android:icon="@mipmap/appicon"
               android:roundIcon="@mipmap/appicon_round"
               android:supportsRtl="true">

    <!-- ★ AdMob App ID -->
    <meta-data android:name="com.google.android.gms.ads.APPLICATION_ID"
               android:value="ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX" />
  </application>

  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  <uses-permission android:name="android.permission.INTERNET" />
</manifest>

⚠️ android:value には AdMob コンソール で取得した App ID(広告ユニットIDではない)を設定します。

Step 3: MauiProgram.cs でプラグインを登録

using Plugin.MauiMtAdmob;

public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .UseMauiMTAdmob()   // ★ これを追加
        .ConfigureFonts(fonts => { /* ... */ });

    // AdService を DI 登録
    builder.Services.AddSingleton<IAdService, AdService>();

    return builder.Build();
}

Step 4: MainActivity.cs で SDK を初期化

ここが最も重要なポイントです。 AdMob SDK の初期化は MainActivity.OnCreate で行います。

using Android.App;
using Android.Content.PM;
using Android.OS;
using Plugin.MauiMtAdmob;
using Plugin.MauiMtAdmob.Extra;

namespace HabitTracker;

[Activity(Theme = "@style/Maui.SplashTheme", MainLauncher = true,
    LaunchMode = LaunchMode.SingleTop,
    ConfigurationChanges = ConfigChanges.ScreenSize | ConfigChanges.Orientation
                         | ConfigChanges.UiMode | ConfigChanges.ScreenLayout
                         | ConfigChanges.SmallestScreenSize | ConfigChanges.Density)]
public class MainActivity : MauiAppCompatActivity
{
    protected override void OnCreate(Bundle? savedInstanceState)
    {
        base.OnCreate(savedInstanceState);

        // AndroidManifest.xml から App ID を取得
        var appInfo = PackageManager?.GetApplicationInfo(
            PackageName!, PackageInfoFlags.MetaData);
        var appId = appInfo?.MetaData?.GetString(
            "com.google.android.gms.ads.APPLICATION_ID") ?? string.Empty;

        CrossMauiMTAdmob.Current.Init(
            activity: this,
            appId: appId,
            license: string.Empty,
            openAdsId: string.Empty,
            nativeAdsId: string.Empty,
            enableOpenAds: false,
            tagForUnderAgeOfConsent: false,
            testDeviceId: string.Empty,
#if DEBUG
            forceTesting: true,    // デバッグ時はテストモード
#else
            forceTesting: false,   // リリース時は本番モード
#endif
            geography: DebugGeography.DEBUG_GEOGRAPHY_DISABLED,
            initialiseConsentAtStartup: false,
            debugMode: false);
    }
}

Step 5: テスト用 / 本番用の広告ユニットID を切り替え

開発中は Google 公式テスト広告 ID を使いましょう。 本番 ID ではエミュレータに広告が配信されません(NO_FILL エラー)。

namespace HabitTracker.Services;

public static class AdUnitIds
{
#if DEBUG
    // Google 公式テスト広告ユニットID(常にテスト広告が配信される)
    public const string Banner = "ca-app-pub-3940256099942544/6300978111";
    public const string Rewarded = "ca-app-pub-3940256099942544/5224354917";
#else
    // 本番広告ユニットID(AdMob コンソールで取得)
    public const string Banner = "ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX";
    public const string Rewarded = "ca-app-pub-XXXXXXXXXXXXXXXX/XXXXXXXXXX";
#endif
}

📝 Google 公式テスト広告ID 一覧

広告タイプ テストID
バナー ca-app-pub-3940256099942544/6300978111
インタースティシャル ca-app-pub-3940256099942544/1033173712
リワード ca-app-pub-3940256099942544/5224354917

バナー広告の実装

バナー広告は XAML に MTAdView コントロールを配置するだけで表示できます。

XAML

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:admob="clr-namespace:Plugin.MauiMtAdmob.Controls;assembly=Plugin.MauiMtAdmob"
             xmlns:services="clr-namespace:HabitTracker.Services"
             x:Class="HabitTracker.Views.MainPageView">

    <Grid RowDefinitions="*, Auto">

        <!-- メインコンテンツ -->
        <ScrollView Grid.Row="0">
            <!-- ... -->
        </ScrollView>

        <!-- ★ バナー広告 -->
        <admob:MTAdView Grid.Row="1"
                        AdsId="{x:Static services:AdUnitIds.Banner}"
                        HorizontalOptions="Fill" />
    </Grid>
</ContentPage>

ポイント:

  • x:StaticAdUnitIds.Banner を参照することで、DEBUG/RELEASE で自動的にIDが切り替わります
  • Grid.Row の最下部に配置するのが一般的です
  • HorizontalOptions="Fill" で画面幅いっぱいに広告を表示します

リワード広告の実装

リワード広告はバナーと違い、ロード → 表示 → 報酬受け取り → クローズ のライフサイクル管理が必要です。

インターフェース定義

namespace HabitTracker.Services;

public interface IAdService
{
    void LoadRewardedAd();
    Task<bool> ShowRewardedAdAsync();
}

AdService 実装

using Plugin.MauiMtAdmob;
using Plugin.MauiMtAdmob.Extra;

namespace HabitTracker.Services;

public class AdService : IAdService
{
    private const string RewardedAdUnitId = AdUnitIds.Rewarded;
    private static readonly TimeSpan LoadTimeout = TimeSpan.FromSeconds(15);
    private static readonly TimeSpan ShowTimeout = TimeSpan.FromSeconds(30);

#if ANDROID || IOS
    private TaskCompletionSource<bool>? _tcs;
    private TaskCompletionSource<bool>? _loadTcs;
    private bool _rewardEarned;
#endif

    public AdService()
    {
        // ★ コンストラクタでは LoadRewardedAd() を呼ばない!
        //   SDK 初期化(MainActivity.Init())の後で呼ぶこと
    }

    public void LoadRewardedAd()
    {
#if ANDROID || IOS
        CrossMauiMTAdmob.Current.LoadRewarded(RewardedAdUnitId);
#endif
    }

    public async Task<bool> ShowRewardedAdAsync()
    {
#if ANDROID || IOS
        // 広告がロードされていなければロードして待つ
        if (!CrossMauiMTAdmob.Current.IsRewardedLoaded())
        {
            _loadTcs = new TaskCompletionSource<bool>();

            CrossMauiMTAdmob.Current.OnRewardedLoaded += OnRewardedAdLoaded;
            CrossMauiMTAdmob.Current.OnRewardedFailedToLoad += OnRewardedAdFailedToLoad;

            LoadRewardedAd();

            var loadCompleted = await Task.WhenAny(
                _loadTcs.Task, Task.Delay(LoadTimeout));

            CrossMauiMTAdmob.Current.OnRewardedLoaded -= OnRewardedAdLoaded;
            CrossMauiMTAdmob.Current.OnRewardedFailedToLoad -= OnRewardedAdFailedToLoad;

            if (loadCompleted != _loadTcs.Task || !_loadTcs.Task.Result)
            {
                return false; // ロード失敗 or タイムアウト
            }
        }

        _rewardEarned = false;
        _tcs = new TaskCompletionSource<bool>();

        CrossMauiMTAdmob.Current.OnUserEarnedReward += OnUserEarnedReward;
        CrossMauiMTAdmob.Current.OnRewardedClosed += OnRewardedClosed;
        CrossMauiMTAdmob.Current.OnRewardedFailedToShow += OnRewardedFailedToShow;

        CrossMauiMTAdmob.Current.ShowRewarded();

        // タイムアウト付きで広告表示完了を待つ
        var showCompleted = await Task.WhenAny(
            _tcs.Task, Task.Delay(ShowTimeout));

        if (showCompleted != _tcs.Task)
        {
            CleanupShowHandlers();
            return false; // タイムアウト
        }

        return _tcs.Task.Result;
#else
        return false;
#endif
    }

#if ANDROID || IOS
    private void OnRewardedAdLoaded(object? sender, EventArgs e)
        => _loadTcs?.TrySetResult(true);

    private void OnRewardedAdFailedToLoad(object? sender, MTEventArgs e)
        => _loadTcs?.TrySetResult(false);

    private void OnUserEarnedReward(object? sender, MTEventArgs e)
        => _rewardEarned = true;

    private void OnRewardedClosed(object? sender, EventArgs e)
    {
        CleanupShowHandlers();
        _tcs?.TrySetResult(_rewardEarned);
        LoadRewardedAd(); // 次回用にプリロード
    }

    private void OnRewardedFailedToShow(object? sender, MTEventArgs e)
    {
        CleanupShowHandlers();
        _tcs?.TrySetResult(false);
    }

    private void CleanupShowHandlers()
    {
        CrossMauiMTAdmob.Current.OnUserEarnedReward -= OnUserEarnedReward;
        CrossMauiMTAdmob.Current.OnRewardedClosed -= OnRewardedClosed;
        CrossMauiMTAdmob.Current.OnRewardedFailedToShow -= OnRewardedFailedToShow;
    }
#endif
}

リワード広告のライフサイクル図

 ┌──────────────┐
 │ LoadRewarded │ ← SDK初期化後にプリロード
 └──────┬───────┘
        ▼
 ┌──────────────────┐   失敗   ┌──────────┐
 │ OnRewardedLoaded │ ───────→ │ 再ロード  │
 └──────┬───────────┘          └──────────┘
        ▼
 ┌──────────────┐
 │ ShowRewarded │ ← ユーザーアクションで呼び出し
 └──────┬───────┘
        ▼
 ┌────────────────────┐
 │ OnUserEarnedReward │ ← ユーザーが広告を最後まで視聴
 └──────┬─────────────┘
        ▼
 ┌────────────────┐
 │ OnRewardedClosed│ ← 広告が閉じられた
 └──────┬─────────┘
        ▼
 ┌──────────────┐
 │ LoadRewarded │ ← 次回用にプリロード
 └──────────────┘

ViewModel での使い方

MVVM パターンに従い、ViewModel から IAdService を DI で注入して使用します。

public partial class MainPageViewModel : ObservableObject
{
    private readonly IAdService _adService;

    public MainPageViewModel(IAdService adService)
    {
        _adService = adService;
    }

    [RelayCommand]
    private async Task LoadHabitsAsync()
    {
        // ... データ読み込み ...

        // SDK 初期化後にリワード広告をプリロード
        _adService.LoadRewardedAd();
    }

    [RelayCommand]
    private async Task NavigateToAddAsync()
    {
        if (IsAtHabitLimit)
        {
            bool confirmed = await Shell.Current.DisplayAlertAsync(
                "追加上限に達しました",
                $"習慣は現在最大 {MaxHabitSlots} 個までです。\n"
                + "広告を見ると1枠追加できます。",
                "広告を見る",
                "キャンセル");

            if (!confirmed) return;

            // ★ リワード広告を表示し、結果を受け取る
            bool rewarded = await _adService.ShowRewardedAdAsync();

            if (rewarded)
            {
                // 報酬付与の処理
                MaxHabitSlots++;
                Preferences.Default.Set(MaxSlotsPrefKey, MaxHabitSlots);
                await Shell.Current.GoToAsync("HabitAdd");
            }
            else
            {
                await Shell.Current.DisplayAlertAsync(
                    "広告エラー",
                    "広告を表示できませんでした。"
                    + "しばらくしてからもう一度お試しください。",
                    "OK");
            }
        }
        else
        {
            await Shell.Current.GoToAsync("HabitAdd");
        }
    }
}

ハマりポイントと解決策

実際の開発で遭遇した問題とその解決策をまとめます。

❌ 1. コンストラクタで広告をロードしてはいけない

問題: AdService のコンストラクタで LoadRewardedAd() を呼ぶと、SDK 初期化前にロードが走り、必ず失敗する。

// ❌ NG: DI でインスタンス生成される = base.OnCreate() の中 = Init() より前
public AdService()
{
    LoadRewardedAd(); // SDK 未初期化でクラッシュ or 無視される
}

解決策: コンストラクタは空にし、SDK 初期化後(画面表示時など)に明示的に LoadRewardedAd() を呼ぶ。

// ✅ OK: ページ表示時にプリロード
[RelayCommand]
private async Task LoadHabitsAsync()
{
    // ... データ読み込み ...
    _adService.LoadRewardedAd(); // SDK 初期化済みのタイミングで呼ぶ
}

❌ 2. 本番 ID ではエミュレータに広告が配信されない

問題: 本番用の広告ユニットIDを使うと、エミュレータでは エラーコード 3(NO_FILL) が返され、広告が一切表示されない。forceTesting: true を設定しても改善しない。

I/Ads: This request is sent from a test device.
I/Ads: Ad failed to load : 3    ← NO_FILL

解決策: #if DEBUGGoogle 公式テスト広告ID に切り替える(前述の AdUnitIds クラス)。

❌ 3. ShowRewarded() に広告ユニットIDを渡してはいけない

問題: Plugin.MauiMTAdmob v2.4.0 では、リワード広告は queueId ベースのキューシステム を使っている。API のパラメータ名を見ないと間違えやすい。

// API シグネチャ
void LoadRewarded(string adUnit, MTRewardedAdOptions? options = null, string queueId = "default");
void ShowRewarded(string queueId = "default");
bool IsRewardedLoaded(string queueId = "default");
// ❌ NG: ShowRewarded のパラメータは queueId であり、広告ユニットIDではない!
CrossMauiMTAdmob.Current.ShowRewarded("ca-app-pub-xxx/yyy");

// ❌ NG: LoadRewarded の第3引数に空文字を渡すと queueId が "default" と不一致
CrossMauiMTAdmob.Current.LoadRewarded(adUnitId, null, string.Empty);
// ✅ OK: すべてデフォルトの queueId = "default" で統一
CrossMauiMTAdmob.Current.LoadRewarded(adUnitId);      // queueId = "default"
CrossMauiMTAdmob.Current.IsRewardedLoaded();           // queueId = "default"
CrossMauiMTAdmob.Current.ShowRewarded();               // queueId = "default"

❌ 4. TaskCompletionSource にタイムアウトがないと UI がフリーズする

問題: ShowRewardedAdAsync()TaskCompletionSource を使って非同期に結果を待つが、イベントが発火しないケースがあると await が永遠に完了しない → UIフリーズ。

解決策: Task.WhenAny + Task.Delay でタイムアウトを設ける。

// ✅ OK: 30秒でタイムアウト
var showCompleted = await Task.WhenAny(_tcs.Task, Task.Delay(TimeSpan.FromSeconds(30)));

if (showCompleted != _tcs.Task)
{
    CleanupShowHandlers();
    return false; // タイムアウト
}

❌ 5. イベントハンドラのクリーンアップ忘れ

問題: OnRewardedClosed 等のイベントを解除しないと、2回目以降の広告表示でハンドラが多重登録され、予期しない動作になる。

解決策: 広告のクローズ・失敗時に必ずハンドラを解除する CleanupShowHandlers() を用意する。


まとめ

最終的なファイル構成

HabitTracker/
├── Services/
│   ├── AdUnitIds.cs        ← DEBUG/RELEASE 切り替え
│   ├── IAdService.cs       ← インターフェース
│   └── AdService.cs        ← リワード広告のロード・表示
├── ViewModels/
│   └── MainPageViewModel.cs ← AdService を DI で利用
├── Views/
│   └── MainPageView.xaml    ← MTAdView でバナー広告表示
├── Platforms/Android/
│   ├── MainActivity.cs      ← SDK 初期化
│   └── AndroidManifest.xml  ← App ID 設定
└── MauiProgram.cs           ← .UseMauiMTAdmob() + DI 登録

チェックリスト

  • [ ] AndroidManifest.xml に AdMob App ID を設定した
  • [ ] MauiProgram.cs.UseMauiMTAdmob() を呼んだ
  • [ ] MainActivity.OnCreateCrossMauiMTAdmob.Current.Init() を呼んだ
  • [ ] DEBUG ビルドでは Google 公式テスト広告 ID を使っている
  • [ ] LoadRewarded / IsRewardedLoaded / ShowRewardedqueueId が統一されている
  • [ ] ShowRewardedAdAsync()タイムアウトを設けている
  • [ ] イベントハンドラを適切にクリーンアップしている
  • [ ] コンストラクタではなく SDK 初期化後 に広告をプリロードしている

この記事が .NET MAUI での AdMob 実装の参考になれば幸いです!

コメント

タイトルとURLをコピーしました