Start Debugging

Monetizing a .NET MAUI app with AdMob (banner, interstitial, rewarded) in 2026

A practical, end-to-end guide to monetizing a .NET MAUI app with Google AdMob using Plugin.AdMob. Install one NuGet, call UseAdMob(), and wire up banner, interstitial, and rewarded ads — with consent built in via UMP, test ad units, the exact service APIs, preloading patterns, and a production checklist. Tested on .NET 10 / MAUI.

Short answer: install the Plugin.AdMob NuGet package, call UseAdMob() on your app builder, and you get all three of the workhorse ad formats — banner (a XAML control), interstitial, and rewarded (both injectable services) — on Android and iOS, with GDPR consent handled for you through Google’s User Messaging Platform. No per-platform handlers, no binding-conflict yak-shaving. This guide wires up all three from an empty project, shows the exact APIs, and covers the parts that actually matter in production: test units, preloading, consent, and frequency.

This is the modern replacement for the hand-rolled banner handler I wrote back in How to: Add AdMob to your MAUI app — that post is still a good read if you want to see what the plugin does for you under the hood, but you no longer need to write any of it.

Fully supported on Android and iOS. On MacCatalyst and Windows the plugin still registers handlers so your app compiles and runs cross-platform — the ad calls are simply no-ops there, so you don’t have to #if them out.

The three formats at a glance

Before the code, pick the right format for the right moment. Mixing them badly is the fastest way to tank retention.

FormatWhat it isGood forThe rule
BannerA small, persistent strip embedded in your layoutContent screens the user dwells on (lists, readers)Keep it out of the way of taps; never over content the user needs
InterstitialA full-screen ad shown at a natural breakBetween levels, after finishing a task, on navigation boundariesShow at transitions, never mid-interaction, and never back-to-back
RewardedFull-screen, opt-in, pays the user a reward on completion”Watch an ad for a hint / extra life / to unlock X”Always user-initiated; grant the reward only from the reward callback

Banner is passive revenue. Interstitial is the highest-yield format you can overdo. Rewarded is the one users actually like, because they chose it and got something for it.

Step 0 — Install, initialize, and configure the platforms

Install the package (this pulls in the Android and iOS Google Mobile Ads bindings — all published on nuget.org now, no local feed needed):

dotnet add package Plugin.AdMob

Register the plugin in MauiProgram.cs. This single call registers IInterstitialAdService, IRewardedAdService, the consent service, and every ad-view handler in the DI container:

public static MauiApp CreateMauiApp()
{
    var builder = MauiApp.CreateBuilder();
    builder
        .UseMauiApp<App>()
        .UseAdMob();

    return builder.Build();
}

Android manifest

In Platforms/Android/AndroidManifest.xml, add the AdMob activity, the network permissions, and your AdMob application ID as a meta-data element inside <application> (the value below is Google’s public test app ID — swap it for your own before shipping):

<manifest ...>
  <application ...>
    <activity android:name="com.google.android.gms.ads.AdActivity"
              android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"
              android:theme="@android:style/Theme.Translucent" />
    <meta-data
        android:name="com.google.android.gms.ads.APPLICATION_ID"
        android:value="ca-app-pub-3940256099942544~3347511713" />
  </application>
  <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
  <uses-permission android:name="android.permission.INTERNET" />
</manifest>

Set the minimum Android version to 23 in your .csproj — Google Play Services requires it, and a lower value throws manifest-merger errors on .NET 10:

<PropertyGroup Condition="'$(TargetFramework)' == 'net10.0-android'">
    <SupportedOSPlatformVersion>23.0</SupportedOSPlatformVersion>
</PropertyGroup>

iOS Info.plist and AppDelegate

Add your GADApplicationIdentifier and the SKAdNetworkItems list (per Google’s iOS quick-start), plus the App Manager flag and App Tracking Transparency string, to Platforms/iOS/Info.plist:

<key>GADIsAdManagerApp</key>
<true/>
<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads to you.</string>
<key>MinimumOSVersion</key>
<string>15.0</string>

Start the Mobile Ads SDK in Platforms/iOS/AppDelegate.cs:

[Register("AppDelegate")]
public class AppDelegate : MauiUIApplicationDelegate
{
    protected override MauiApp CreateMauiApp()
    {
        var app = MauiProgram.CreateMauiApp();
        Google.MobileAds.MobileAds.SharedInstance.Start(completionHandler: null);
        return app;
    }
}

Set the minimum iOS version to 15.0 (the SDK requires it — without it MobileAds.Start can NullReferenceException on device), and, from plugin 2.0.0 onward, add the Swift-linking target to your .csproj. Both snippets are in the Setup docs; I’ll skip them here to keep this readable.

Use test ads during development

Do not click your own live ads — AdMob will flag the account. Instead, flip one switch and the plugin routes every request to Google’s safe-to-tap test units:

using Plugin.AdMob.Configuration;

AdConfig.UseTestAdUnitIds = true;

With that set, you can omit AdUnitId everywhere and still get test creatives. Set it from a debug guard so it never ships enabled:

#if DEBUG
AdConfig.UseTestAdUnitIds = true;
#endif

When testing on a physical device against your real units, register it as a test device instead so you get test creatives without risking your account:

AdConfig.AddTestDevice("<your-test-device-id>");

Since plugin 1.3.0, consent is handled automatically through Google’s User Messaging Platform. If the user is in a region that requires it, the consent form shows on startup, and ad requests are held until consent is resolved. You don’t have to write anything — but you do have to configure the messages in AdMob’s Privacy & messaging section, or the form never appears and (correctly) no ads load. Full control lives in IAdConsentService; see the Consent docs. In 2026 this is not optional polish — it’s the difference between serving ads and serving nothing across the EEA and UK.

A banner is a XAML control. Declare the namespace and drop it into a layout — ideally pinned to the bottom of a content page so it doesn’t fight your UI:

<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:admob="clr-namespace:Plugin.AdMob;assembly=Plugin.AdMob"
             x:Class="MyApp.FeedPage">
    <Grid RowDefinitions="*,Auto">
        <!-- your content -->
        <CollectionView Grid.Row="0" ItemsSource="{Binding Items}" />

        <!-- ad pinned to the bottom -->
        <admob:BannerAd Grid.Row="1"
                        AdUnitId="ca-app-pub-xxxxxxxxxxxxxxxx/xxxxxxxxxx"
                        AdSize="Banner" />
    </Grid>
</ContentPage>

AdUnitId is optional when AdConfig.UseTestAdUnitIds is true. Pick a size with the AdSize property:

AdSizeWidth × HeightNotes
SmartBannerScreen width × 32/50/90Adapts height to the device
Banner320 × 50The classic
LargeBanner320 × 100
MediumRectangle300 × 250Higher yield; use in-feed, not as a strip
FullBanner468 × 60Tablet
Leaderboard728 × 90Tablet
CustomCustomAdWidth × CustomAdHeightSee below

For a custom size, set AdSize="Custom" and supply the dimensions in density-independent pixels:

<admob:BannerAd AdSize="Custom" CustomAdWidth="200" CustomAdHeight="200" />

The control exposes an IsLoaded property and a full set of events, so you can react to the ad’s lifecycle — for example, only reveal the container once an ad actually fills:

<admob:BannerAd x:Name="Banner"
                AdUnitId="ca-app-pub-xxxxxxxxxxxxxxxx/xxxxxxxxxx"
                OnAdLoaded="OnBannerLoaded"
                OnAdFailedToLoad="OnBannerFailed" />
private void OnBannerLoaded(object? sender, EventArgs e)
    => Banner.IsVisible = true;   // no empty gap while it loads or if it fails

private void OnBannerFailed(object? sender, EventArgs e)
    => Banner.IsVisible = false;

The available banner events:

EventWhen it fires
OnAdLoadedAn ad was received
OnAdFailedToLoadThe request failed
OnAdImpressionAn impression was recorded
OnAdClickedA click was recorded
OnAdOpenedThe ad opened a screen-covering overlay
OnAdClosedThe user is about to return to your app after a click
OnAdSwipedA swipe was counted as a click (Android only)

Interstitial ads

Interstitials are full-screen, so they’re a service, not a view. Two things matter: preload before you need it, and only show a loaded ad. Fetching an ad takes a network round-trip; if you call show-on-demand you’ll get nothing.

Inject IInterstitialAdService (or resolve it from the service provider), preload with PrepareAd(), and show at your transition:

using Plugin.AdMob.Services;

public partial class GamePage : ContentPage
{
    private readonly IInterstitialAdService _interstitialAds;

    public GamePage(IInterstitialAdService interstitialAds)
    {
        InitializeComponent();
        _interstitialAds = interstitialAds;

        // Preload early — e.g. when the level starts — so the ad is ready
        // by the time the level ends.
        _interstitialAds.PrepareAd("ca-app-pub-xxxxxxxxxxxxxxxx/xxxxxxxxxx");
    }

    private void OnLevelCompleted()
    {
        if (_interstitialAds.IsAdLoaded)
            _interstitialAds.ShowAd();   // no-op if nothing is prepared
    }
}

If you prefer not to use constructor injection, grab it from the container:

var interstitialAds = IPlatformApplication.Current!.Services
    .GetRequiredService<IInterstitialAdService>();

The service interface:

public interface IInterstitialAdService
{
    bool IsAdLoaded { get; }

    IInterstitialAd CreateAd(string? adUnitId = null);
    void PrepareAd(string? adUnitId = null);
    void ShowAd();

    event EventHandler OnAdLoaded;
}

PrepareAd() and CreateAd() both fall back to AdConfig.DefaultInterstitialAdUnitId when you pass no ID (more on defaults below). IsAdLoaded is true once an ad is ready; the service-level OnAdLoaded event fires at the same moment if you’d rather show reactively than poll.

Re-preloading after dismiss (the CreateAd path)

The single-slot PrepareAd/ShowAd pair is the easy path, but it has no dismiss callback — after an ad is shown, you need to call PrepareAd() again for the next one. When you want per-ad lifecycle control (to preload the next interstitial the moment the current one closes), use CreateAd() and drive the returned IInterstitialAd yourself:

public interface IInterstitialAd
{
    string AdUnitId { get; }
    bool IsLoaded { get; }

    event EventHandler OnAdLoaded;
    event EventHandler<IAdError> OnAdFailedToLoad;
    event EventHandler OnAdShowed;
    event EventHandler<IAdError> OnAdFailedToShow;
    event EventHandler OnAdImpression;
    event EventHandler OnAdClicked;
    event EventHandler OnAdDismissed;

    void Load();
    void Show();
}

A self-refilling interstitial that’s always ready for the next transition:

private IInterstitialAd? _ad;

private void PreloadInterstitial()
{
    _ad = _interstitialAds.CreateAd("ca-app-pub-xxxxxxxxxxxxxxxx/xxxxxxxxxx");

    // As soon as one is dismissed, load the next.
    _ad.OnAdDismissed += (_, _) => PreloadInterstitial();
    _ad.OnAdFailedToLoad += (_, err) =>
        System.Diagnostics.Debug.WriteLine($"Interstitial failed: {err.Message}");

    _ad.Load();
}

private void ShowInterstitialIfReady()
{
    if (_ad?.IsLoaded == true)
        _ad.Show();
}

Frequency matters more than any code here. Cap interstitials to natural breaks, put a cooldown between them (a couple of minutes, or every N transitions), and never show one on app launch or mid-gesture. Google’s own guidance is that too-frequent interstitials increase uninstalls; frequency capping in the AdMob console is worth setting too.

Rewarded ads

Rewarded ads are the polite format: the user opts in (“Watch an ad to earn 50 coins”), and you grant the reward only when the reward callback fires — not when the ad is shown, and not when it’s dismissed. The plugin gives you the reward through a callback on PrepareAd, or an OnUserEarnedReward event on the ad instance.

The simplest path mirrors interstitials, with one extra parameter — the reward callback:

using Plugin.AdMob;          // RewardItem
using Plugin.AdMob.Services;

public partial class StorePage : ContentPage
{
    private readonly IRewardedAdService _rewardedAds;

    public StorePage(IRewardedAdService rewardedAds)
    {
        InitializeComponent();
        _rewardedAds = rewardedAds;
        PreloadReward();
    }

    private void PreloadReward()
    {
        // The onUserEarnedReward callback fires only if the user completes the ad.
        _rewardedAds.PrepareAd(
            "ca-app-pub-xxxxxxxxxxxxxxxx/xxxxxxxxxx",
            onUserEarnedReward: reward => GrantReward(reward));
    }

    private void OnWatchAdForCoinsClicked(object sender, EventArgs e)
    {
        if (_rewardedAds.IsAdLoaded)
            _rewardedAds.ShowAd();
        else
            DisplayAlert("Not ready", "No ad available right now — try again shortly.", "OK");
    }

    private void GrantReward(RewardItem reward)
    {
        // reward.Type and reward.Amount come from your AdMob ad unit config.
        Wallet.Add(reward.Amount);
        PreloadReward();   // preload the next one
    }
}

RewardItem is a small record — the values come from how you configured the reward on the ad unit in the AdMob console:

public sealed record RewardItem(int Amount, string Type);

The service interface:

public interface IRewardedAdService
{
    bool IsAdLoaded { get; }

    IRewardedAd CreateAd(string? adUnitId = null);
    void PrepareAd(string? adUnitId = null, Action<RewardItem>? onUserEarnedReward = null);
    void ShowAd();

    event EventHandler OnAdLoaded;
}

When you pass no ID it falls back to AdConfig.DefaultRewardedAdUnitId. As with interstitials, CreateAd() returns a fully controllable IRewardedAd when you want per-ad events — including the dedicated OnUserEarnedReward:

public interface IRewardedAd
{
    string AdUnitId { get; }
    bool IsLoaded { get; }

    event EventHandler OnAdLoaded;
    event EventHandler<IAdError> OnAdFailedToLoad;
    event EventHandler OnAdShowed;
    event EventHandler<IAdError> OnAdFailedToShow;
    event EventHandler OnAdImpression;
    event EventHandler OnAdClicked;
    event EventHandler OnAdDismissed;
    event EventHandler<RewardItem> OnUserEarnedReward;

    void Load();
    void Show();
}

The critical distinction: OnAdDismissed is not the same as OnUserEarnedReward. A user can dismiss the ad early without earning anything. Grant value from OnUserEarnedReward only — treating dismissal as completion is how people farm free rewards.

var ad = _rewardedAds.CreateAd("ca-app-pub-xxxxxxxxxxxxxxxx/xxxxxxxxxx");

ad.OnUserEarnedReward += (_, reward) => Wallet.Add(reward.Amount);   // ✅ grant here
ad.OnAdDismissed      += (_, _) => PreloadNextReward();               // ❌ NOT here

ad.Load();

Set default ad unit IDs once

Threading ad unit ID strings through every call site gets old. Set them once via AdConfig (or pass them to UseAdMob(...)), then call PrepareAd() / CreateAd() with no arguments:

using Plugin.AdMob.Configuration;

AdConfig.DefaultBannerAdUnitId       = "ca-app-pub-.../...";
AdConfig.DefaultInterstitialAdUnitId = "ca-app-pub-.../...";
AdConfig.DefaultRewardedAdUnitId     = "ca-app-pub-.../...";

Now _interstitialAds.PrepareAd() and _rewardedAds.PrepareAd(onUserEarnedReward: GrantReward) resolve their units from config. Keep separate IDs per platform (Android and iOS ad units are different) and select at runtime, or store them in your platform config.

Going to production — the checklist

Gotchas checklist

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Back