AdMob in Flutter: google_mobile_ads Step by Step
Set up google_mobile_ads in a Flutter app: Android and iOS config, consent, test ads, and short working banner, interstitial and rewarded examples.
12 min read · Updated
To show AdMob ads in a Flutter app you add the google_mobile_ads package, paste your AdMob app ID into AndroidManifest.xml and Info.plist, call MobileAds.instance.initialize(), and then load banner, interstitial or rewarded ad objects with your ad unit IDs. That is the whole shape of it. The work is in the details: which ID goes where, when to load, when to dispose, and how to keep the first test build from earning you a policy warning.
At the time of writing the package is at version 9.2.0 and Google's quick start asks for Flutter 3.38.1 or newer (the version moved up in the 8.0.0 release, and the summary at the top of the quick start page still shows an older number, so trust the prerequisites section and your own flutter pub add output). Run it against your own project and read the changelog before you copy anything from a tutorial that is more than a year old. Several size helpers and the Swift Package Manager story have changed.
This guide covers the Android and iOS configuration, consent, test ads, and short working examples of the three formats most apps actually use. Money figures are modelled estimates from our benchmark tables, not promises.
What to set up in AdMob first
You need three things from the AdMob console before the code means anything.
First, an AdMob account with your app added. Add the Android and iOS versions as two separate apps, even though they share a Dart codebase. Each gets its own app ID, which looks like ca-app-pub-1234567890123456~1234567890 (note the tilde).
Second, one ad unit per format per platform. A banner unit, an interstitial unit and a rewarded unit on Android, and the same three on iOS, make six IDs. Ad unit IDs have a slash instead of a tilde. Mixing up the two is the most common beginner mistake, and the SDK will refuse to start or return an invalid request error.
Third, a plan for what happens before your app is approved and has history. A new app often serves few ads for a while. That is normal. Read how to read your AdMob report so you know which numbers to trust in the first week.
If you only want to try the code, you can skip the console entirely for now. Google publishes demo IDs that always return test ads, and every snippet below uses them.
Step 1: add the plugin
From the project root:
flutter pub add google_mobile_ads
That writes the dependency into pubspec.yaml with the current version. The package is published by Google on pub.dev and supports Android and iOS only. There is no web or desktop implementation, so if your app also targets those, guard every ad call with a platform check.
Import it wherever you use ads:
import 'package:google_mobile_ads/google_mobile_ads.dart';
Step 2: add the app ID on each platform
Android manifest
Open android/app/src/main/AndroidManifest.xml and add a meta-data tag inside the application element:
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3940256099942544~3347511713"/>
The value shown is Google's sample Android app ID. Swap in your own before you release. If the ID is missing or malformed, expect the app to fail at startup on Android, and check this tag first when it does.
If the build later complains about the minimum SDK level, raise minSdk in android/app/build.gradle to the level the error message names. Flutter's own default has moved over the years and the plugin may require more than an older project template sets.
The Flutter plugin wraps the native Android SDK, so any Android-specific advice you find for AdMob applies here too.
iOS Info.plist
On iOS, open ios/Runner/Info.plist and add:
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy</string>
This one has no universal sample in the Flutter quick start, so use your real iOS app ID from the console. A debug build with a real app ID but test ad unit IDs is fine, and it is the normal way to work.
The Flutter quick start only asks for that key. If you later add mediation partners, each partner's own setup page may ask for extra Info.plist entries, including SKAdNetwork identifiers, and you should follow those. Our iOS integration guide lists what the native SDK expects, and ATT and iOS ad earnings explains the tracking prompt and why it moves iOS revenue.
You will also want a working app-ads.txt file for each store, and the Google Play version is the place to start. Buyers use it to check that you are an authorised seller, and without it fill falls.
Step 3: ask for consent, then initialize
Initialization is one line:
await MobileAds.instance.initialize();
The returned future completes when every adapter has finished starting up, or after 30 seconds, whichever comes first. You do not have to wait for it before building your UI, but you should not request ads until it has been called.
The better question is what must happen first. If any of your users are in the European Economic Area, the UK or Switzerland, you need a consent message, and Google's User Messaging Platform (UMP) is the free way to show one. The plugin includes it. The flow Google documents is: request a consent update on every launch, load and show the form if one is required, then check canRequestAds() before touching the ad APIs. The docs state that canRequestAds() returns false until requestConsentInfoUpdate() has been called.
A compact version:
Future<void> startAds() async {
Future<void> initIfAllowed() async {
if (await ConsentInformation.instance.canRequestAds()) {
await MobileAds.instance.initialize();
}
}
ConsentInformation.instance.requestConsentInfoUpdate(
ConsentRequestParameters(),
() {
ConsentForm.loadAndShowConsentFormIfRequired((FormError? error) {
initIfAllowed();
});
},
(FormError error) {
// Falls back to the status saved from the previous session.
initIfAllowed();
},
);
}
Call startAds() from main() after WidgetsFlutterBinding.ensureInitialized(), or from your first screen's initState. Two extras are worth adding. One is a debug block while you test, using ConsentDebugSettings with DebugGeography.debugGeographyEea and your test device's hashed ID, which forces the form to appear even if you are sitting in Texas. Delete it before release. The other is a privacy options entry in your settings screen: check ConsentInformation.instance.getPrivacyOptionsRequirementStatus() and, if it reports required, show a button that calls ConsentForm.showPrivacyOptionsForm.
The longer story on regulations, including what the message must say and how TCF works, sits in our consent messages guide. Google's own privacy and consent page for Flutter has the complete sample. If your app is aimed at children, stop and read ads in kids apps before you ship anything, because the request configuration is different.
Turn on test ads before you load anything
Every snippet in this guide uses ca-app-pub-3940256099942544/... IDs. These are Google's demo units. They always fill and they never earn or cost anything.
| Format | Android | iOS |
|---|---|---|
| Banner | ca-app-pub-3940256099942544/9214589741 | ca-app-pub-3940256099942544/2435281174 |
| Interstitial | ca-app-pub-3940256099942544/1033173712 | ca-app-pub-3940256099942544/4411468910 |
| Rewarded | ca-app-pub-3940256099942544/5224354917 | ca-app-pub-3940256099942544/1712485313 |
The banner IDs above come from the adaptive banner page and are the ones used in the sample code there. Google's separate test ads page lists a different sample banner pair, so check which one you copy against which page you are following. Both are demo units.
Emulators and simulators count as test devices automatically. A physical phone does not, and clicking live ads on your own device is a fast route to an invalid traffic flag. Run the app once, find the test device ID line in logcat or the Xcode console, and register it:
MobileAds.instance.updateRequestConfiguration(
RequestConfiguration(testDeviceIds: ['YOUR_TEST_DEVICE_ID']),
);
Do this before the first ad request, and remove it before release. When it works you will see a "Test Ad" label on banner, interstitial and rewarded ads. Mediated ads do not carry that label, so each mediation partner needs its own test mode.
The plugin can also open Ad Inspector on a registered test device with MobileAds.instance.openAdInspector, which takes a callback that receives an error if something went wrong. Ad Inspector is the quickest way to see what a request actually did, so use it before you start guessing.
Banner ads
A banner is the simplest format and the lowest paying. In our tables AdMob banners sit around $0.90 eCPM in Tier 1 on Android, against $8.00 for an interstitial and $12.00 for rewarded. That does not make them pointless, since a banner can serve many impressions a session, but it does mean you should not contort your layout around one. The banner format page has the full range.
Use an adaptive size. The older helper methods were deprecated in the 8.0.0 release in favour of AdSize.getLargeAnchoredAdaptiveBannerAdSize, which takes the available width and returns a size with an appropriate height. It returns a future that can resolve to null, so handle that.
class BannerSlot extends StatefulWidget {
const BannerSlot({super.key});
@override
State<BannerSlot> createState() => _BannerSlotState();
}
class _BannerSlotState extends State<BannerSlot> {
static final _unitId = Platform.isAndroid
? 'ca-app-pub-3940256099942544/9214589741'
: 'ca-app-pub-3940256099942544/2435281174';
BannerAd? _ad;
bool _requested = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_requested) {
_requested = true;
_load();
}
}
Future<void> _load() async {
final size = await AdSize.getLargeAnchoredAdaptiveBannerAdSize(
MediaQuery.sizeOf(context).width.truncate(),
);
if (size == null || !mounted) return;
BannerAd(
adUnitId: _unitId,
request: const AdRequest(),
size: size,
listener: BannerAdListener(
onAdLoaded: (ad) {
if (!mounted) {
ad.dispose();
return;
}
setState(() => _ad = ad as BannerAd);
},
onAdFailedToLoad: (ad, error) => ad.dispose(),
),
).load();
}
@override
void dispose() {
_ad?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final ad = _ad;
if (ad == null) return const SizedBox.shrink();
return SizedBox(
width: ad.size.width.toDouble(),
height: ad.size.height.toDouble(),
child: AdWidget(ad: ad),
);
}
}
You need import 'dart:io'; at the top for Platform. Drop BannerSlot at the bottom of a Scaffold inside a SafeArea.
Two rules from Google's banner docs are easy to miss. An AdWidget needs a parent with a fixed width and height, or iOS may not draw it. And treat each BannerAd as belonging to a single AdWidget: when the widget leaves the tree, dispose the ad and load a new one rather than reusing it. For lists that scroll, the docs point to inline adaptive banners, with a warning that they can perform poorly on Android 9 and earlier.
Interstitial ads
Interstitials are full-screen and single use. You load one, hold the reference, show it at a natural break, and dispose it the moment it closes. Then you load the next. Google's docs are explicit that each InterstitialAd can be shown once, and a second call to show() triggers the failure callback.
class InterstitialLoader {
static final _unitId = Platform.isAndroid
? 'ca-app-pub-3940256099942544/1033173712'
: 'ca-app-pub-3940256099942544/4411468910';
InterstitialAd? _ad;
void load() {
InterstitialAd.load(
adUnitId: _unitId,
request: const AdRequest(),
adLoadCallback: InterstitialAdLoadCallback(
onAdLoaded: (ad) {
ad.fullScreenContentCallback = FullScreenContentCallback(
onAdDismissedFullScreenContent: (ad) {
ad.dispose();
_ad = null;
load();
},
onAdFailedToShowFullScreenContent: (ad, error) {
ad.dispose();
_ad = null;
load();
},
);
_ad = ad;
},
onAdFailedToLoad: (error) => _ad = null,
),
);
}
bool showIfReady() {
final ad = _ad;
if (ad == null) return false;
ad.show();
return true;
}
}
Call load() once after initialization, then call showIfReady() between levels, after a result screen, or when the user finishes a task. Do not retry inside onAdFailedToLoad without a delay. A tight retry loop produces a pile of requests that all fail, and it drags your match rate down. Our fill rate guide explains why that matters.
Where you place the call matters more than the code does. An interstitial that appears when someone taps a button they meant to use will earn a click that the advertiser does not value, a one-star review, or both. The rewarded vs interstitial guide goes through placement logic, and the interstitial format page shows the price range.
There is also a newer, beta-labelled preloading API, InterstitialAdPreloader, which keeps a small buffer of ads ready and refills it for you. Google's ad preloading page only shows interstitial examples, so treat it as optional until the docs cover more.
Rewarded ads
A rewarded ad is opt-in. The user taps something like "Watch a video for 50 coins", and the reward is granted only when the callback fires. The load pattern matches interstitials, with a different callback and a required reward handler.
RewardedAd? _rewarded;
void loadRewarded() {
RewardedAd.load(
adUnitId: Platform.isAndroid
? 'ca-app-pub-3940256099942544/5224354917'
: 'ca-app-pub-3940256099942544/1712485313',
request: const AdRequest(),
rewardedAdLoadCallback: RewardedAdLoadCallback(
onAdLoaded: (ad) {
ad.fullScreenContentCallback = FullScreenContentCallback(
onAdDismissedFullScreenContent: (ad) {
ad.dispose();
_rewarded = null;
loadRewarded();
},
onAdFailedToShowFullScreenContent: (ad, error) {
ad.dispose();
_rewarded = null;
},
);
_rewarded = ad;
},
onAdFailedToLoad: (error) => _rewarded = null,
),
);
}
void showRewarded(void Function(int amount) grant) {
_rewarded?.show(
onUserEarnedReward: (AdWithoutView ad, RewardItem reward) {
grant(reward.amount.toInt());
},
);
}
show takes a named, required onUserEarnedReward parameter and returns a future. The reward item comes from the ad unit's settings in the console, so the amount and type you configure there arrive in RewardItem. Most apps ignore those values and grant their own, which is fine as long as you do it only in the earned callback.
If you give out anything with real value, such as premium currency, consider server-side verification. The plugin supports it through ServerSideVerificationOptions and setServerSideOptions on the ad, with the callback itself configured in AdMob. For a casual game with soft currency, it is usually more work than it is worth.
Rewarded video pays the most per impression and users like it because they choose it. Our rewarded format page shows the range by region.
Lifecycle mistakes that cost money
Most Flutter ad bugs are lifecycle bugs, and the same few come up again and again.
Forgetting to dispose. Every BannerAd, InterstitialAd and RewardedAd holds native resources. Dispose banners in the widget's dispose, and full-screen ads in the dismissed and failed-to-show callbacks.
Loading inside build. The method runs many times. Put the load in initState, didChangeDependencies with a guard like the one above, or a service class.
Showing before the ad exists. Keep a nullable reference and check it. Do not block the UI waiting for it.
Rebuilding the AdWidget with a new ad every frame. Hold the ad in state and only replace it deliberately.
Requesting too early or too often. Ads requested before consent or initialization finish, or on screens the user never reaches, create unfilled requests that make your match rate look worse than the real demand.
App open ads are a fifth format worth knowing about. They use AppStateEventNotifier to show an ad when the app returns to the foreground, and the docs warn that a loaded ad expires after four hours. Show them sparingly. Native ads exist too, through ready-made templates styled in Dart or custom platform layouts, and are a better fit for feeds than for a game menu.
What these placements might earn
Here is a simple example with stated assumptions. It is not a forecast. Take a casual app (our category multiplier is 0.95) with 5,000 daily active users, all in Tier 1 on Android, and assume the ads fill.
| Format | Impressions per day | eCPM (Tier 1) | Daily revenue |
|---|---|---|---|
| Rewarded, 1 per user | 5,000 | $12.00 | $60.00 |
| Interstitial, 2 per user | 10,000 | $8.00 | $80.00 |
| Banner, 8 per user | 40,000 | $0.90 | $36.00 |
That adds up to $176.00, and after the 0.95 category multiplier it is about $167 a day, or roughly $0.033 per daily user. Over 30 days that is near $5,000. Real audiences are rarely all Tier 1. A mixed audience with a lot of Tier 3 traffic can earn a fraction of that, because Tier 3 rewarded eCPM in our table is $1.40 against $12.00. Put your own numbers into the AdMob revenue calculator.
Common questions
Does google_mobile_ads work on Flutter web?
No. The package supports Android and iOS. If you ship a web build from the same codebase, wrap ad code in platform checks or conditional imports so it never runs there.
Why does my ad fail to load with a no fill error?
No fill means the request went through and nothing came back. With test IDs it usually points to a mistyped unit ID or a platform mix-up, such as an Android ID on iOS. With real IDs on a new app it is common during the first days. Print the LoadAdError in onAdFailedToLoad and check Ad Inspector before changing anything else.
Can I use real ad unit IDs while developing?
You can, but you should not tap the ads. Use test IDs or register your device as a test device so your clicks never count. Self-clicks are one of the reasons accounts get limited, which why ad accounts get limited goes through.
Should I add mediation now?
Not on day one. Get the single-network setup working, ship, and then read the AdMob mediation setup guide when you have traffic.
What to do next
Swap the demo IDs for your real ones in a build config so debug builds keep the test units and release builds get live units. A common way is to put them behind kReleaseMode or a --dart-define flag, so you never ship a release with a demo ID or a debug build with a live one.
Then publish to a closed test track, check that app-ads.txt is in place, and watch the AdMob console for a couple of days before deciding anything about eCPM. If the numbers look low, why is my eCPM low has a diagnostic order. If the same game also exists in Unity, ads in Unity games covers that stack.