Ads versus subscriptions is the oldest monetization debate in indie mobile, and in 2026 the honest answer is still "it depends." Subscriptions win on ARPU for the same engaged user. Ads win on revenue per install for utility, casual, and high-volume apps where most users will never pay anything. AdMob remains the default ad network for Flutter because the official google_mobile_ads package is well-maintained, mediation is mature, and the fill rate beats almost everyone except Meta Audience Network. This guide is the full 2026 setup: banner, interstitial, rewarded, native, ATT on iOS, UMP consent, mediation, and the patterns for combining ads with subscriptions cleanly.
Short version: install google_mobile_ads 5.x, wire theMobileAds.instance.initialize() call after the UMP consent flow has resolved, gate iOS ad personalization behind the ATT prompt, show banners on content surfaces, interstitials at natural breaks, rewarded ads for opt-in value exchange, and route paying users around the entire ad layer. Add mediation as soon as you hit 10k DAU.
The 2026 AdMob stack for Flutter
The official package is google_mobile_ads, maintained by the Google Mobile Ads team. In 2026 the relevant versions are 5.x, which require Flutter 3.27+ and target Android SDK 35. The package wraps the native Android and iOS AdMob SDKs and exposes Dart-friendly APIs for every ad format.
What ships in the box:
- Ad formats: banner, interstitial, rewarded, rewarded interstitial, native, app open.
- Mediation: AppLovin MAX, ironSource, Meta Audience Network, Mintegral, Unity Ads, Liftoff.
- UMP SDK: Google's consent management for GDPR and US state laws.
- App Open Ads: full-screen ads when the app returns from background.
- Privacy: ATT plumbing on iOS, Privacy Sandbox support on Android.
Setup: AdMob account, app ID, test ad units
Three things to do before writing Dart:
- Create an AdMob account at admob.google.com and register your app to get an App ID for iOS and Android.
- Create ad units for each format you plan to use. Each unit has its own ad unit ID per platform.
- During development use Google's test ad unit IDs (documented on the AdMob site). Never test against production IDs because impressions count and you can get banned for self-clicking.
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
google_mobile_ads: ^5.2.0
app_tracking_transparency: ^2.0.6 # iOS ATT promptWire the App ID in both platforms. On Android, editandroid/app/src/main/AndroidManifest.xml:
<application>
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3940256099942544~3347511713"/>
</application>On iOS, add to ios/Runner/Info.plist:
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-3940256099942544~1458002511</string>
<key>SKAdNetworkItems</key>
<array>
<!-- Paste the full SKAdNetwork list from AdMob docs (75+ entries in 2026) -->
</array>Forgetting the SKAdNetwork list on iOS silently kills attribution and your eCPM drops 30 to 50 percent. Copy the full block from Google's docs each time it updates.
Initialize the SDK once, after consent has been collected:
import 'package:google_mobile_ads/google_mobile_ads.dart';
Future<void> initAds() async {
await MobileAds.instance.initialize();
await MobileAds.instance.updateRequestConfiguration(
RequestConfiguration(
tagForChildDirectedTreatment: TagForChildDirectedTreatment.unspecified,
maxAdContentRating: MaxAdContentRating.t,
),
);
}iOS specific: ATT prompt and IDFA
App Tracking Transparency landed in iOS 14.5 and is still the single biggest revenue lever on iOS in 2026. The prompt asks the user to allow tracking across apps and websites. Allow gives you the IDFA, which AdMob uses for personalized ads and attribution. Deny strips personalization and roughly halves effective eCPM.
import 'package:app_tracking_transparency/app_tracking_transparency.dart';
Future<void> requestAttIfNeeded() async {
final status = await AppTrackingTransparency.trackingAuthorizationStatus;
if (status == TrackingStatus.notDetermined) {
// Apple requires a pre-prompt explanation before the system prompt
await showAttExplainerSheet();
await AppTrackingTransparency.requestTrackingAuthorization();
}
}Two rules that keep you out of App Review trouble:
- Add the
NSUserTrackingUsageDescriptionstring toInfo.plist. Without it the prompt never shows and Apple rejects. - Do not gate any app functionality behind allow. Apple rejects anything that punishes the user for denying tracking (no "please allow to continue" blockers).
Best practice in 2026 is to show a custom pre-prompt that explains the value (free app supported by personalized ads) and only show the system ATT prompt after the user taps continue. Acceptance rates with a good pre-prompt are 35 to 55 percent versus 18 to 25 percent without.
Android specific: Privacy Sandbox and Data Safety
Privacy Sandbox on Android replaced the advertising ID for users on Android 13+ who opt out. google_mobile_ads 5.x handles the Topics API, Protected Audience, and Attribution Reporting automatically. You only need to:
- Declare the AD_ID permission in
AndroidManifest.xml:<uses-permission android:name="com.google.android.gms.permission.AD_ID"/> - Fill out the Play Console Data Safety form: declare that you collect advertising ID, device identifiers, app interactions, and that data is shared with third parties (Google, mediation networks).
- Link your app to a privacy policy URL that covers AdMob, mediation partners, and the user's rights under GDPR and CPRA.
Banner ads: anchored, inline, adaptive
Three banner styles, each with a different use case:
| Format | Typical eCPM | UX impact | Where to place |
|---|---|---|---|
| Anchored banner | $0.20 - $1.50 | Low, ignored fast | Persistent at bottom of utility screens |
| Inline adaptive banner | $0.40 - $2.50 | Medium, blends with content | Inside feed lists, between content cards |
| Interstitial | $3 - $12 | High, full screen | Between levels, after content view |
| Rewarded | $8 - $25 | Low, opt-in | Unlock content, extra lives, hints |
| Rewarded interstitial | $5 - $15 | Medium, opt-out before play | Natural breaks where reward fits |
| Native ad | $1 - $5 | Low, looks like content | Feeds, search results, settings |
| App open ad | $2 - $8 | Very high, intrusive | Cold start and warm resume (use sparingly) |
Adaptive banners size themselves to the available width and Google's recommended height for that width, which usually beats the legacy fixed 320x50 by 20 to 40 percent on eCPM. Use them unless you have a specific reason not to.
class AdaptiveBannerWidget extends StatefulWidget {
const AdaptiveBannerWidget({super.key, required this.adUnitId});
final String adUnitId;
@override
State<AdaptiveBannerWidget> createState() => _AdaptiveBannerWidgetState();
}
class _AdaptiveBannerWidgetState extends State<AdaptiveBannerWidget> {
BannerAd? _ad;
bool _loaded = false;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_loadAd();
}
Future<void> _loadAd() async {
final size = await AdSize.getCurrentOrientationAnchoredAdaptiveBannerAdSize(
MediaQuery.of(context).size.width.truncate(),
);
if (size == null) return;
final ad = BannerAd(
adUnitId: widget.adUnitId,
size: size,
request: const AdRequest(),
listener: BannerAdListener(
onAdLoaded: (_) => setState(() => _loaded = true),
onAdFailedToLoad: (ad, err) => ad.dispose(),
),
);
await ad.load();
_ad = ad;
}
@override
void dispose() {
_ad?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!_loaded || _ad == null) return const SizedBox.shrink();
return SizedBox(
width: _ad!.size.width.toDouble(),
height: _ad!.size.height.toDouble(),
child: AdWidget(ad: _ad!),
);
}
}Always call dispose() in State.dispose. A leakedBannerAd keeps the native ad view alive and eventually crashes on memory pressure.
Interstitial ads: timing, frequency capping, app-open ads
Interstitials pay well but burn user goodwill fast. The 2026 rules of thumb from indie devs who actually measure retention impact:
- Frequency cap to one interstitial per 3 to 5 minutes of session time.
- Never show during onboarding. Apple has rejected apps for this in 2024 and 2025.
- Show on natural breaks: after a level ends, after a chapter, after returning from a detail screen, not mid-task.
- Preload the next ad immediately after dismissing the previous one to avoid the 1 to 3 second loading delay.
class InterstitialManager {
InterstitialAd? _ad;
final String adUnitId;
DateTime? _lastShownAt;
static const _minInterval = Duration(minutes: 4);
InterstitialManager(this.adUnitId) {
_load();
}
void _load() {
InterstitialAd.load(
adUnitId: adUnitId,
request: const AdRequest(),
adLoadCallback: InterstitialAdLoadCallback(
onAdLoaded: (ad) {
_ad = ad;
ad.fullScreenContentCallback = FullScreenContentCallback(
onAdDismissedFullScreenContent: (ad) {
ad.dispose();
_ad = null;
_load();
},
onAdFailedToShowFullScreenContent: (ad, err) {
ad.dispose();
_ad = null;
_load();
},
);
},
onAdFailedToLoad: (err) => _ad = null,
),
);
}
Future<void> maybeShow() async {
if (_ad == null) return;
final now = DateTime.now();
if (_lastShownAt != null && now.difference(_lastShownAt!) < _minInterval) return;
_lastShownAt = now;
await _ad!.show();
}
}App-open ads are full-screen ads that show when the user returns to your app from background. They are aggressive and Google warns that overuse will downrank you in the Play Store. Use only on cold starts longer than 4 hours since last use, and never on the first launch.
Rewarded ads: integrating with in-app rewards
Rewarded ads are the highest-eCPM format because they are opt-in. The user trades 15 to 30 seconds of attention for an in-app reward: extra lives, a hint, a one-day pro trial, an unlocked feature. Rewarded inventory in 2026 still clears at $8 to $25 eCPM in tier-1 countries.
Future<void> showRewarded({
required String adUnitId,
required VoidCallback onReward,
}) async {
final completer = Completer<void>();
RewardedAd.load(
adUnitId: adUnitId,
request: const AdRequest(),
rewardedAdLoadCallback: RewardedAdLoadCallback(
onAdLoaded: (ad) {
ad.fullScreenContentCallback = FullScreenContentCallback(
onAdDismissedFullScreenContent: (ad) {
ad.dispose();
if (!completer.isCompleted) completer.complete();
},
onAdFailedToShowFullScreenContent: (ad, err) {
ad.dispose();
if (!completer.isCompleted) completer.completeError(err);
},
);
ad.show(onUserEarnedReward: (ad, reward) => onReward());
},
onAdFailedToLoad: (err) => completer.completeError(err),
),
);
await completer.future;
}Three production patterns worth stealing:
- Server-side verification: enable AdMob server-side verification so the reward is granted by your backend, not the client. Stops trivial cheating.
- Preload before the user reaches the reward UI. Loading after the tap adds 2 seconds of dead time and tanks completion.
- If load fails, give the reward anyway on the first failure. Users hate the "ad failed to load" popup and a free reward costs less than the lost session.
Native ads: matching your design system
Native ads render as Flutter widgets you build, so they can match your typography, spacing, and corner radius. Done well, native ads are nearly invisible to users and convert better than banners. Done badly, they look like a banner with extra steps.
class NativeAdCard extends StatefulWidget {
const NativeAdCard({super.key, required this.adUnitId});
final String adUnitId;
@override
State<NativeAdCard> createState() => _NativeAdCardState();
}
class _NativeAdCardState extends State<NativeAdCard> {
NativeAd? _ad;
bool _loaded = false;
@override
void initState() {
super.initState();
_ad = NativeAd(
adUnitId: widget.adUnitId,
factoryId: 'listTile',
request: const AdRequest(),
listener: NativeAdListener(
onAdLoaded: (_) => setState(() => _loaded = true),
onAdFailedToLoad: (ad, err) => ad.dispose(),
),
)..load();
}
@override
void dispose() {
_ad?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (!_loaded) return const SizedBox.shrink();
return ConstrainedBox(
constraints: const BoxConstraints(minHeight: 72, maxHeight: 120),
child: AdWidget(ad: _ad!),
);
}
}The factoryId maps to a platform-native ad layout that you register in Swift (iOS) and Kotlin (Android). The package docs include a complete listTile example. Always render the AdChoices icon and the "Ad" or "Sponsored" label clearly, AdMob policy requires it.
Ad mediation: AppLovin MAX, ironSource, and higher eCPM
Mediation lets a single ad request fan out to multiple networks (AdMob, AppLovin, Meta, Unity, ironSource) and the highest bidder fills. Above 10k DAU, mediation typically lifts revenue by 20 to 40 percent because of the second-price auction dynamics.
Two paths in 2026:
- AdMob mediation: stay in the AdMob dashboard, add adapter packages for each network. Simplest to manage, slightly lower revenue than MAX.
- AppLovin MAX: replace AdMob as the SDK entirely, with AdMob as a mediated source. Generally 5 to 15 percent higher revenue in 2026, more complex setup.
Start with AdMob mediation. Add AppLovin, Meta, Unity, and Liftoff adapter packages, enable them in the AdMob console, and the SDK handles the waterfall. Switch to MAX only once you have measurable scale and the engineering time to migrate.
GDPR / CMP and UMP SDK consent flow
For EEA, UK, and Swiss traffic, you must present a Google-certified CMP before serving any personalized ad. Google's own UMP SDK is bundled withgoogle_mobile_ads. Configure a consent message in the AdMob console (one-time UI work) and call:
import 'package:google_mobile_ads/google_mobile_ads.dart';
Future<void> ensureConsent() async {
final params = ConsentRequestParameters();
await ConsentInformation.instance.requestConsentInfoUpdate(params, () async {
if (await ConsentInformation.instance.isConsentFormAvailable()) {
final form = await ConsentForm.loadConsentForm();
if (await ConsentInformation.instance.getConsentStatus() == ConsentStatus.required) {
await form.show();
}
}
}, (err) {});
if (await ConsentInformation.instance.canRequestAds()) {
await MobileAds.instance.initialize();
}
}Do not initialize AdMob before canRequestAds() returns true on first launch in EEA. Doing so loads non-consented ads and exposes you to GDPR enforcement. The same flow satisfies California CPRA and the other US state laws for 2026.
Combining ads and subscriptions: remove ads for paying users
The most common 2026 monetization stack for indie apps is hybrid: free tier with ads, paid tier with no ads plus premium features. Wire it through a singleSubscriptionGate that the ad widgets check before loading anything.
class AdGatedBanner extends StatelessWidget {
const AdGatedBanner({super.key, required this.adUnitId});
final String adUnitId;
@override
Widget build(BuildContext context) {
return BlocBuilder<SubscriptionBloc, SubscriptionState>(
builder: (context, state) {
if (state is SubscriptionActive) return const SizedBox.shrink();
return AdaptiveBannerWidget(adUnitId: adUnitId);
},
);
}
}Two patterns from production:
- Check the subscription state synchronously from a cached RevenueCat or Superwall stream. Network calls per banner kill scroll performance.
- For interstitials and rewarded, also check entitlement before loading the ad, not just before showing it. Loading a never-shown ad wastes user bandwidth and slightly hurts your eCPM through low CTR.
The math on ads versus subscriptions in 2026 for the same engaged user is roughly: subscription ARPU is 3 to 8x ad ARPU per active user. But subscriptions convert at 1 to 4 percent of installs, while ads monetize 100 percent. The hybrid model wins because you collect ad revenue from the 96 to 99 percent who never pay, and subscription revenue from the few who do.
Measuring revenue: AdMob console, Firebase, Adjust
AdMob console gives you ad impressions, eCPM, fill rate, and revenue per country. Firebase (linked from AdMob) gives you the per-user revenue plus retention cross-references. Attribution tools like Adjust, AppsFlyer, and Singular show which ad campaigns produced which monetized users.
Minimum analytics stack for any ad-monetized Flutter app:
- AdMob linked to Firebase for per-user ARPU.
- Firebase Analytics events for every reward, interstitial show, and consent decision.
- An attribution SDK (Adjust or AppsFlyer) if you spend on UA.
- A weekly review of fill rate by country and eCPM by ad unit. Reorder waterfall lines based on real data.
Anti-patterns to avoid
- Do not show an interstitial during onboarding. Apple has rejected multiple Flutter apps for this in 2024 and 2025. Wait until the user has completed at least one core task.
- Do not show app-open ads on cold start of a new install. First-impression users bounce when an ad is the first thing they see, and Play Store flags repeat offenders.
- Do not skip ATT or UMP. Skipping ATT halves iOS revenue. Skipping UMP risks GDPR fines that dwarf your annual ad income.
- Do not click your own ads on a real ad unit. AdMob bans accounts for this and there is no appeal process worth relying on. Always use test ad unit IDs in development.
- Do not stack two banners on the same screen. AdMob policy violation and it tanks CTR.
- Do not show ads to paying users. Check the subscription state in every ad widget. One leaked banner kills churn metrics.
- Do not forget to dispose ads. Leaked
BannerAd,InterstitialAd, andNativeAdobjects retain native memory and eventually OOM on low-end devices.
What The Flutter Kit ships
The Flutter Kit ships an opt-in AdMob layer with the full 2026 plumbing: google_mobile_ads 5.x pre-configured for iOS and Android, the UMP consent flow wired before SDK init, an ATT pre-prompt and system prompt, adaptive banner, interstitial manager with frequency capping, rewarded ad helper with server-side verification hooks, a native ad layout for iOS and Android, and aSubscriptionGate that automatically hides every ad widget for paying users. Mediation adapters for AppLovin, Meta, and Unity are included as optional flags.
One-time payment of $69 for unlimited commercial projects. See the full integration list on the features page or jump to checkout.
Final recommendation
For utility, casual, and high-volume apps in 2026, AdMob plus subscriptions is the default stack. Start with adaptive banners and rewarded ads, add interstitials at natural breaks once retention is solid, and skip app-open ads until you have a specific reason to push revenue. Layer mediation in at 10k DAU. Most important, treat the ad layer as a feature users tolerate, not the product. The apps that respect the user's attention end up monetizing better because they retain longer, which compounds across every ad format.