ad_flow - Professional AdMob Integration for Flutter
A production-ready, fully compliant AdMob integration package for Flutter with GDPR, US Privacy, and iOS ATT support.
✨ Features
| Feature | Status | Description |
|---|---|---|
| Banner Ads | ✅ | Adaptive banners that fit any screen |
| Collapsible Banners | ✅ | Expandable banners for higher engagement |
| Interstitial Ads | ✅ | Full-screen ads with smart cooldown |
| Rewarded Ads | ✅ | Video ads that reward users |
| App Open Ads | ✅ | Ads on app launch/resume |
| GDPR Consent | ✅ | EU/UK/Switzerland compliance |
| US Privacy | ✅ | CCPA and state regulations |
| iOS ATT | ✅ | App Tracking Transparency |
| Native Ads | ✅ | Custom ads matching your app design |
| Mediation | ✅ | Unity Ads, AppLovin, and more |
| Remove Ads | ✅ | Built-in IAP support to disable ads |
| Auto Preloading | ✅ | Ads ready when you need them |
| Retry Logic | ✅ | Exponential backoff on failures |
| Lazy Loading | ✅ | Managers created only when used |
| Error Handling | ✅ | Centralized error stream for all ads |
📦 Installation
1. Add Dependency
# pubspec.yaml
dependencies:
ad_flow: ^1.3.8
2. Android Setup
android/app/src/main/AndroidManifest.xml:
<manifest>
<application>
<!-- AdMob App ID -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX"/>
</application>
</manifest>
3. iOS Setup
📱 ios/Runner/Info.plist (click to expand)
<!-- AdMob App ID -->
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-XXXXXXXXXXXXXXXX~XXXXXXXXXX</string>
<!-- App Tracking Transparency -->
<key>NSUserTrackingUsageDescription</key>
<string>This identifier will be used to deliver personalized ads to you.</string>
<!-- SKAdNetwork IDs (required for iOS 14+) -->
<key>SKAdNetworkItems</key>
<array>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>cstr6suwn9.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4fzdc2evr5.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4pfyvq9l8r.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>2fnua5tdw4.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ydx93a7ass.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>5a6flpkh64.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>p78aez3dza.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>v72qych5uu.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>c6k4g5qg8m.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>s39g8k73mm.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3qy4746246.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>3sh42y64q3.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>f38h382jlk.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>hs6bdukanm.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>prcb7njmu6.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>wzmmz9fp6w.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>yclnxrl5pm.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>7ug5zh24hu.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>9rd848q2bz.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>n6fk4nfna4.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>kbd757ywx3.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>9t245vhmpl.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>4468km3ulz.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>m8dbw4sv7c.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>zmvfpc5aq8.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>ejvt5qm6ak.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>5lm9lj6jb7.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>44jx6755aq.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>t38b2kh725.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>24t9a8vw3c.skadnetwork</string>
</dict>
</array>
🚀 Quick Start
⚠️ Important: Initialize Only ONCE
AdFlow is a singleton - you only need to initialize it once for your entire app, typically on your first screen (splash or home page). All other pages can simply use AdFlow.instance to show ads.
┌─────────────────────────────────────────────────────────────┐
│ YOUR APP │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Page 1 │ │ Page 2 │ │ Page 3 │ │
│ │ (Splash) │ │ (Home) │ │ (Details) │ │
│ │ │ │ │ │ │ │
│ │ initialize() │───▶│ showBanner() │───▶│ showBanner() │ │
│ │ ✅ │ │ ✅ │ │ ✅ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └────────────────────┴────────────────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ AdFlow │ │
│ │ (Singleton) │ │
│ │ │ │
│ │ • BannerManager │ │
│ │ • Interstitial │ │
│ │ • AppOpenAd │ │
│ │ • Consent │ │
│ └───────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
| Action | Where | How Often |
|---|---|---|
initialize() |
First page only | Once per app launch |
| Show ads | Any page | As needed |
Initialize in main.dart
import 'package:flutter/material.dart';
import 'package:ad_flow/ad_flow.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize AdMob with your ad unit IDs
await AdFlow.instance.initialize(
config: AdFlowConfig(
// Your production ad unit IDs from AdMob console
androidBannerAdUnitId: 'ca-app-pub-YOUR_ID/BANNER_ID',
iosBannerAdUnitId: 'ca-app-pub-YOUR_ID/BANNER_ID',
androidInterstitialAdUnitId: 'ca-app-pub-YOUR_ID/INTERSTITIAL_ID',
iosInterstitialAdUnitId: 'ca-app-pub-YOUR_ID/INTERSTITIAL_ID',
androidRewardedAdUnitId: 'ca-app-pub-YOUR_ID/REWARDED_ID',
iosRewardedAdUnitId: 'ca-app-pub-YOUR_ID/REWARDED_ID',
androidAppOpenAdUnitId: 'ca-app-pub-YOUR_ID/APP_OPEN_ID',
iosAppOpenAdUnitId: 'ca-app-pub-YOUR_ID/APP_OPEN_ID',
androidNativeAdUnitId: 'ca-app-pub-YOUR_ID/NATIVE_ID',
iosNativeAdUnitId: 'ca-app-pub-YOUR_ID/NATIVE_ID',
),
onComplete: (canRequestAds) {
debugPrint('Ads ready: $canRequestAds');
},
);
runApp(const MyApp());
}
Initialization Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
config |
AdFlowConfig? |
testMode() |
Your ad unit IDs configuration |
onComplete |
Function(bool)? |
null |
Callback when initialization completes |
preloadInterstitial |
bool |
false |
Preload interstitial ad on init |
preloadRewarded |
bool |
false |
Preload rewarded ad on init |
preloadAppOpen |
bool |
false |
Preload app open ad on init |
showAppOpenOnColdStart |
bool |
false |
Show app open ad on first launch |
enableAppOpenOnForeground |
bool |
false |
Show app open ad when app returns from background |
maxForegroundAdsPerSession |
int |
1 |
Max app open ads per session (foreground only) |
Advanced Initialization
// Full control over initialization
await AdFlow.instance.initialize(
config: AdFlowConfig(
androidBannerAdUnitId: 'ca-app-pub-YOUR_ID/BANNER_ID',
iosBannerAdUnitId: 'ca-app-pub-YOUR_ID/BANNER_ID',
// ... other ad unit IDs
),
onComplete: (canRequestAds) {
debugPrint('Ads ready: $canRequestAds');
},
preloadInterstitial: true, // Preload interstitial for faster display
preloadRewarded: true, // Preload rewarded for faster display
preloadAppOpen: true, // Preload app open ad
showAppOpenOnColdStart: true, // Show ad on first app launch
enableAppOpenOnForeground: true, // Show ad when returning to app
maxForegroundAdsPerSession: 2, // Allow 2 foreground app open ads
);
Test Mode (Development)
// For development/testing, use test mode:
await AdFlow.instance.initialize(
config: AdFlowConfig.testMode(), // Uses Google's test ad IDs
);
📱 Usage Examples
Banner Ads (Easiest Way)
import 'package:ad_flow/ad_flow.dart';
// Just drop this widget anywhere!
@override
Widget build(BuildContext context) {
return Scaffold(
body: YourContent(),
// One line for a banner ad:
bottomNavigationBar: const EasyBannerAd(),
);
}
Banner Ads (With More Control)
class MyPage extends StatefulWidget {
@override
State<MyPage> createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
final BannerAdManager _bannerManager = BannerAdManager();
@override
void initState() {
super.initState();
_loadBanner();
}
Future<void> _loadBanner() async {
await _bannerManager.loadAdaptiveBanner(
context: context,
onAdLoaded: (ad) => setState(() {}),
onAdFailedToLoad: (ad, error) {
debugPrint('Banner failed: ${error.message}');
},
);
}
@override
void dispose() {
_bannerManager.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: YourContent(),
bottomNavigationBar: _bannerManager.isLoaded
? _bannerManager.buildAdWidget()
: const SizedBox.shrink(),
);
}
}
Collapsible Banner Ads
// Using EasyBannerAd widget
const EasyBannerAd(collapsible: true)
// Or with BannerAdManager
await _bannerManager.loadCollapsibleBanner(
context: context,
placement: CollapsibleBannerPlacement.bottom, // or .top
onAdLoaded: (ad) => setState(() {}),
);
Custom Size Banner Ads
// Load a specific size banner (great for dialogs)
await _bannerManager.loadBanner(
size: AdSize.mediumRectangle, // 300x250
onAdLoaded: (ad) => setState(() {}),
);
// Other common sizes:
// AdSize.banner // 320x50
// AdSize.largeBanner // 320x100
// AdSize.mediumRectangle // 300x250 (best for dialogs)
// AdSize.fullBanner // 468x60
// AdSize.leaderboard // 728x90
Interstitial Ads
// Show interstitial (auto-preloaded on init)
await AdFlow.instance.interstitial.showAd(
onAdDismissed: () {
// Continue with your app
Navigator.pushNamed(context, '/nextScreen');
},
onAdFailedToShow: () {
// Ad not ready, proceed anyway
Navigator.pushNamed(context, '/nextScreen');
},
);
// Check if ready before showing
if (AdFlow.instance.interstitial.isLoaded) {
AdFlow.instance.interstitial.showAd();
}
Interstitial with Frequency Control
int _actionCount = 0;
void _onUserAction() {
_actionCount++;
// Show interstitial every 5 actions
if (_actionCount % 5 == 0) {
if (AdFlow.instance.interstitial.isLoaded) {
AdFlow.instance.interstitial.showAd();
}
}
}
Rewarded Ads
Rewarded ads let users watch video ads in exchange for in-app rewards (coins, extra lives, etc.):
// Load a rewarded ad (usually done at app start or before needed)
await AdFlow.instance.rewarded.loadAd(
onAdLoaded: (ad) {
print('Rewarded ad ready!');
},
onAdFailedToLoad: (error) {
print('Failed to load rewarded ad: ${error.message}');
},
);
// Show the rewarded ad when user clicks "Watch Ad" button
await AdFlow.instance.rewarded.showAd(
onUserEarnedReward: (reward) {
// Grant the reward to the user
setState(() {
_userCoins += reward.amount.toInt();
});
print('User earned ${reward.amount} ${reward.type}');
},
onAdDismissed: () {
// Ad closed (whether reward earned or not)
print('Rewarded ad dismissed');
},
onAdFailedToShow: () {
// Ad not available
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('No reward available. Try again later!')),
);
},
);
Preload Rewarded Ad on Init
// Add to your initialization for faster ad display
await AdFlow.instance.initialize(
config: AdFlowConfig(...),
preloadRewarded: true, // Preload rewarded ad
);
Check Load Status & Listen for Changes
// Check if rewarded ad is ready
if (AdFlow.instance.rewarded.isLoaded) {
// Show "Watch Ad" button
ElevatedButton(
onPressed: _watchAdForReward,
child: const Text('Watch Ad for 50 Coins'),
);
}
// Listen for ad load status changes (for reactive UI)
@override
void initState() {
super.initState();
AdFlow.instance.rewarded.addStatusListener(_onAdStatusChanged);
}
void _onAdStatusChanged() {
setState(() {}); // Rebuild UI when ad loads/changes
}
@override
void dispose() {
AdFlow.instance.rewarded.removeStatusListener(_onAdStatusChanged);
super.dispose();
}
Manual Reload After Showing
// Rewarded ads auto-reload after being shown, but you can also manually reload:
if (!AdFlow.instance.rewarded.isLoaded &&
!AdFlow.instance.rewarded.isLoading) {
await AdFlow.instance.rewarded.loadAd();
}
App Open Ads
App open ads are automatically handled when you set enableAppOpenOnForeground: true during initialization. They show when the user brings your app to the foreground.
// Manual control (if needed)
if (AdFlow.instance.appOpen.isAdAvailable) {
await AdFlow.instance.appOpen.showAdIfAvailable(
onAdDismissed: () {
// App resumed
},
);
}
Privacy Settings Button
// Check if user needs privacy options (GDPR regions)
if (AdFlow.instance.isPrivacyOptionsRequired) {
IconButton(
icon: const Icon(Icons.privacy_tip),
onPressed: () {
AdFlow.instance.showPrivacyOptions(
onComplete: () {
// User updated privacy settings
},
);
},
);
}
Ad Inspector (Debug Mode)
// Open the Ad Inspector for debugging
AdFlow.instance.openAdInspector();
Remove Ads (In-App Purchase)
Built-in support for "Remove Ads" purchases:
// After successful IAP purchase
await AdFlow.instance.disableAds();
// All ad widgets automatically hide!
// EasyBannerAd, EasyNativeAd, etc. respect this setting.
// Check if ads are enabled
if (AdFlow.instance.isAdsEnabled) {
// Show ads
}
// Re-enable ads (e.g., restore purchase failed)
await AdFlow.instance.enableAds();
// Reactive UI with StreamBuilder
StreamBuilder<bool>(
stream: AdFlow.instance.adsEnabledStream,
builder: (context, snapshot) {
final adsEnabled = snapshot.data ?? true;
if (!adsEnabled) return const SizedBox.shrink();
return const EasyBannerAd();
},
)
Error Handling
Centralized error handling for all ad operations:
// Stream-based (recommended for reactive apps)
AdFlow.instance.errorStream.listen((error) {
print('Ad error: ${error.type} - ${error.message}');
// Log to analytics
analytics.logEvent('ad_error', {
'type': error.type.name, // bannerLoad, interstitialLoad, etc.
'code': error.code, // Error code from SDK
'message': error.message, // Human-readable message
'adUnitId': error.adUnitId, // Which ad unit failed
});
});
// Callback-based (simpler alternative)
AdFlow.instance.setErrorCallback((error) {
crashlytics.recordError(error.originalError ?? error.message);
});
// Clear callback when done
AdFlow.instance.clearErrorCallback();
Error Types:
| Type | Description |
|---|---|
consent |
Consent gathering failed |
bannerLoad |
Banner ad failed to load |
interstitialLoad |
Interstitial failed to load |
interstitialShow |
Interstitial failed to show |
appOpenLoad |
App open ad failed to load |
appOpenShow |
App open ad failed to show |
rewardedLoad |
Rewarded ad failed to load |
rewardedShow |
Rewarded ad failed to show |
nativeLoad |
Native ad failed to load |
sdkInitialization |
SDK initialization failed |
Native Ads Setup
Native ads require platform-specific factory code. See the full guide:
Quick overview:
- Create native ad factories (Kotlin for Android, Swift for iOS)
- Register factories in
MainActivity.kt/AppDelegate.swift - Create layout XML (Android) or XIB files (iOS)
- Use in Flutter with
factoryId
// Load native ad
await AdFlow.instance.native.loadAd(
factoryId: 'medium_template', // Must match registered factory
onAdLoaded: (ad) => setState(() {}),
);
// Display
if (AdFlow.instance.native.isLoaded) {
SizedBox(
height: 300,
child: AdWidget(ad: AdFlow.instance.native.nativeAd!),
)
}
Privacy Settings Button (GDPR Requirement)
GDPR requires providing users a way to modify their consent. Use these widgets in your settings screen:
// Simple button - auto shows/hides based on GDPR requirement
EasyPrivacySettingsButton()
// With custom text
EasyPrivacySettingsButton(
text: 'Manage Privacy',
icon: Icons.shield,
)
// For settings screens - ListTile version
PrivacySettingsListTile(
title: 'Privacy Settings',
subtitle: 'Manage your ad preferences',
)
// Always visible (ignores GDPR check)
PrivacySettingsListTile(
alwaysShow: true,
)
// Fully custom widget
EasyPrivacySettingsButton(
child: YourCustomWidget(),
onFormDismissed: () {
print('User updated privacy settings');
},
)
📂 Package Structure
lib/
├── ad_flow.dart # Barrel export (import this)
└── src/
├── ad_config.dart # Configuration & ad unit IDs
├── ad_error_handler.dart # Centralized error handling
├── ad_service.dart # Main AdFlow service (singleton)
├── ads_enabled_manager.dart # Remove Ads feature
├── consent_manager.dart # GDPR/ATT consent handling
├── consent_explainer_dialog.dart # Pre-consent explainer dialogs
├── consent_explainer_localizations.dart # Multi-language support
├── banner_ad_manager.dart # Banner ad management
├── easy_banner_widget.dart # Drop-in banner widget
├── easy_privacy_settings_button.dart # GDPR privacy settings button
├── interstitial_ad_manager.dart # Interstitial ad management
├── rewarded_ad_manager.dart # Rewarded ad management
├── app_open_ad_manager.dart # App open ad management
├── app_lifecycle_reactor.dart # App state monitoring
├── native_ad_manager.dart # Native ad management
└── native_ad_widget.dart # Drop-in native ad widgets
⚙️ Configuration
Behavior Settings
Customize ad behavior via AdFlowConfig:
await AdFlow.instance.initialize(
config: AdFlowConfig(
// Your ad unit IDs...
androidBannerAdUnitId: 'ca-app-pub-xxx/xxx',
iosBannerAdUnitId: 'ca-app-pub-xxx/xxx',
// Behavior settings
appOpenAdMaxCacheDuration: Duration(hours: 4), // Google recommends max 4 hours
minInterstitialInterval: Duration(seconds: 30), // Cooldown between interstitials
maxLoadRetries: 3, // Retry failed loads
retryDelay: Duration(seconds: 5), // Delay between retries
// Testing & debug
testDeviceIds: ['YOUR_DEVICE_HASHED_ID'], // Avoid invalid impressions
enableConsentDebug: false, // Test GDPR in non-EU regions
),
);
Test Device IDs
Add your test device ID to avoid invalid impressions during development:
config: AdFlowConfig(
// ... ad unit IDs
testDeviceIds: ['YOUR_DEVICE_HASHED_ID'],
),
Find your device ID in the console logs:
I/Ads: Use RequestConfiguration.Builder().setTestDeviceIds(Arrays.asList("YOUR_DEVICE_ID"))
🔒 Privacy & Compliance
GDPR (Europe)
- ✅ Automatically shows consent form for EU/UK/Switzerland users
- ✅ Uses Google's certified UMP SDK
- ✅ Stores consent for future sessions
- ✅ Respects user's privacy choices
US Privacy (CCPA)
- ✅ Supports US state privacy regulations
- ✅ Handles opt-out requests
iOS ATT (App Tracking Transparency)
- ✅ Integrated with consent flow
- ✅ Shows system permission dialog
- ✅ Respects user's tracking choice
How It Works
App Start
│
▼
┌─────────────────────┐
│ Check Consent Status │
└─────────────────────┘
│
▼ (if GDPR region)
┌─────────────────────┐
│ Show Consent Form │
└─────────────────────┘
│
▼ (if iOS)
┌─────────────────────┐
│ ATT Permission │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Initialize Ads SDK │
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Preload Ads │
└─────────────────────┘
Pre-Consent Explainer (Better UX)
For a friendlier user experience, you can show an explainer dialog before the official consent popups appear. This gives users context about why they're being asked for consent.
// Option 1: Initialize with explainer (recommended for better UX)
class _MyHomePageState extends State<MyHomePage> {
@override
void initState() {
super.initState();
// Show explainer after first frame renders
WidgetsBinding.instance.addPostFrameCallback((_) {
AdFlow.instance.initializeWithExplainer(
context: context,
onComplete: (canRequestAds) {
debugPrint('Ads ready: $canRequestAds');
},
);
});
}
}
// Option 2: Standard initialization (consent popups appear immediately)
await AdFlow.instance.initialize(
onComplete: (canRequestAds) {
// Ready
},
);
The explainer shows:
- 🎯 General privacy explainer - "Your Privacy Matters" with benefits
- 📱 iOS ATT explainer - Brief explanation before the system ATT popup
You can also show the dialogs manually:
// Show the general consent explainer
await ConsentExplainerDialog.show(context);
// Show the iOS ATT explainer (iOS only)
await ATTExplainerDialog.show(context);
Multi-Language Support
Built-in localized texts for consent explainers:
| Language | Consent Texts | ATT Texts |
|---|---|---|
| English (default) | kDefaultConsentExplainerTexts |
kDefaultATTExplainerTexts |
| Persian (فارسی) | kPersianConsentExplainerTexts |
kPersianATTExplainerTexts |
| Spanish (Español) | kSpanishConsentExplainerTexts |
kSpanishATTExplainerTexts |
// Use pre-defined language texts
AdFlow.instance.initializeWithExplainer(
context: context,
consentTexts: kPersianConsentExplainerTexts,
attTexts: kPersianATTExplainerTexts,
onComplete: (canRequestAds) {
debugPrint('Ads ready: $canRequestAds');
},
);
// Or get texts by language code
final (consentTexts, attTexts) = getExplainerTextsForLanguage('es');
AdFlow.instance.initializeWithExplainer(
context: context,
consentTexts: consentTexts,
attTexts: attTexts,
);
// Create custom texts for any language
const myCustomTexts = ConsentExplainerTexts(
title: 'Your Title',
description: 'Your description...',
benefitRelevantAds: 'Relevant ads',
benefitDataSecure: 'Data stays secure',
benefitKeepFree: 'Keeps app free',
settingsHint: 'Change anytime in Settings.',
continueButton: 'Continue',
skipButton: 'Decide later',
);
💰 Revenue Optimization Tips
1. Ad Placement Best Practices
| Do ✅ | Don't ❌ |
|---|---|
| Place banners at natural content breaks | Cover content with ads |
| Show interstitials at natural pauses | Show interstitials during gameplay |
| Use app open ads on cold start | Show too many app open ads |
| Test different placements | Ignore user experience |
2. Interstitial Frequency
// Default cooldown is 30 seconds (configurable via AdFlowConfig)
// Recommended: Show at natural breaks, not too frequently
minInterstitialInterval: Duration(seconds: 30),
3. Banner Refresh
Banners automatically refresh every 60 seconds (AdMob default). Don't manually refresh more frequently.
4. Fill Rate Optimization
- ✅ Use adaptive banners (auto-sizes)
- ✅ Keep HTTP timeout at 30 seconds
- ✅ Implement retry logic (included)
- ✅ Test on real devices
5. eCPM Optimization
- ✅ Enable all ad formats
- ✅ Use mediation (see below)
- ✅ Target appropriate content rating
- ✅ Maintain high user engagement
🔗 Mediation Support
Maximize revenue by serving ads from multiple networks. ad_flow supports Unity Ads, AppLovin, and any other AdMob mediation network.
Quick Setup
import 'package:ad_flow/ad_flow.dart';
import 'package:gma_mediation_unity/gma_mediation_unity.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
// Register BEFORE AdFlow.initialize()
final unity = GmaMediationUnity();
MediationHelper.registerUnityWithCallbacks(
setGDPRConsent: unity.setGDPRConsent,
setCCPAConsent: unity.setCCPAConsent,
);
// Consent is auto-forwarded during initialization
await AdFlow.instance.initialize(...);
}
📖 Full guide: See doc/MEDIATION_SETUP.md for complete setup instructions, supported networks, and troubleshooting.
🔍 API Reference
AdFlow
// Singleton instance
AdFlow.instance
// Properties
bool isInitialized // SDK initialized?
bool isMobileAdsInitialized // Mobile Ads ready?
bool isPrivacyOptionsRequired // Show privacy button?
bool isAdsEnabled // Ads enabled? (Remove Ads)
bool isAdsDisabled // Ads disabled?
// Managers
ConsentManager consent // Consent handling
BannerAdManager banner // Banner ads
InterstitialAdManager interstitial // Interstitial ads
RewardedAdManager rewarded // Rewarded ads
AppOpenAdManager appOpen // App open ads
NativeAdManager native // Native ads
// Methods
Future<void> initialize({...}) // Initialize everything
Future<void> disableAds() // Disable ads (Remove Ads)
Future<void> enableAds() // Re-enable ads
void showPrivacyOptions({...}) // Show privacy form
void openAdInspector() // Debug tool
// Streams
Stream<bool> adsEnabledStream // Reactive ads enabled state
BannerAdManager
// Properties
bool isLoaded // Banner ready?
bool isLoading // Loading in progress?
BannerAd? bannerAd // The ad object
// Methods
Future<void> loadAdaptiveBanner({...}) // Load adaptive banner
Future<void> loadCollapsibleBanner({...}) // Load collapsible banner
Widget? buildAdWidget() // Get AdWidget
void dispose() // Clean up
InterstitialAdManager
// Properties
bool isLoaded // Ad ready?
bool isLoading // Loading in progress?
bool isShowing // Currently displayed?
bool canShowAd // Cooldown passed?
// Methods
Future<void> loadAd({...}) // Load interstitial
Future<bool> showAd({...}) // Show interstitial
void dispose() // Clean up
AppOpenAdManager
// Properties
bool isLoaded // Ad loaded?
bool isAdAvailable // Ready & not expired?
// Methods
Future<void> loadAd({...}) // Load app open ad
Future<void> showAdIfAvailable({...}) // Show if available
void dispose() // Clean up
RewardedAdManager
// Properties
bool isLoaded // Ad ready?
bool isLoading // Loading in progress?
bool isShowing // Currently displayed?
// Methods
Future<void> loadAd({...}) // Load rewarded ad
Future<bool> showAd({...}) // Show rewarded ad
void addStatusListener(cb) // Listen for load status
void removeStatusListener(cb) // Remove listener
void dispose() // Clean up
EasyBannerAd Widget
const EasyBannerAd({
bool collapsible = false, // Use collapsible format?
})
NativeAdManager
// Properties
bool isLoaded // Ad loaded?
bool isLoading // Loading in progress?
NativeAd? nativeAd // The ad object
// Methods
Future<void> loadAd({...}) // Load native ad
void dispose() // Clean up
EasyNativeAd Widget
const EasyNativeAd({
required String factoryId, // Native ad factory ID
required double height, // Ad height
double? width, // Ad width (optional)
Widget? loadingWidget, // Loading placeholder
Widget? errorWidget, // Error placeholder
bool hideOnLoading = true, // Collapse while loading
bool hideOnError = true, // Collapse on error/no fill
EdgeInsets padding, // Padding around ad
Color? backgroundColor, // Background color
BorderRadius? borderRadius, // Corner radius
VoidCallback? onAdLoaded, // Callback when ad loads
VoidCallback? onAdFailedToLoad, // Callback on load failure
})
Collapse Behavior (v1.3.6+): By default, EasyNativeAd collapses to zero height when loading or when an ad fails to load (e.g., no fill). This prevents empty white space in fixed-height layouts like bottomNavigationBar. Set hideOnLoading: false or hideOnError: false to show placeholder widgets instead.
Example with callbacks:
EasyNativeAd(
factoryId: 'medium_template',
height: 300,
onAdLoaded: () => debugPrint('Native ad loaded!'),
onAdFailedToLoad: () => debugPrint('Native ad failed to load'),
)
AdsEnabledManager
// Singleton instance
AdsEnabledManager.instance
// Properties
bool isEnabled // Ads enabled?
bool isDisabled // Ads disabled?
// Methods
Future<void> disableAds() // Disable all ads
Future<void> enableAds() // Re-enable ads
void addListener(callback) // Listen for changes
void removeListener(callback) // Remove listener
// Stream
Stream<bool> stream // Reactive state changes
🐛 Troubleshooting
Ads Not Loading
- Check internet connection
- Verify ad unit IDs are correct
- Wait 24-48 hours after creating new ad units
- Check logs for error codes:
- Error 0: Internal error
- Error 1: Invalid request
- Error 2: Network error
- Error 3: No fill
Consent Form Not Showing
- Form only shows in GDPR regions (EU/UK/Switzerland)
- Use VPN to test from GDPR region
- Add test device ID for consent debugging
iOS Build Errors
- Run
pod installin ios folder - Update minimum iOS version to 13.0+
- Ensure Info.plist has all required keys
Android Build Errors
- Check
minSdkVersionis 21+ - Ensure AndroidManifest.xml has App ID
- Run
flutter clean && flutter pub get
📋 Checklist Before Release
UseAdFlowConfigwith your production ad unit IDsRemovetestDeviceIdsor leave emptySetenableConsentDebug: falseTest on real devicesTest consent flow in GDPR region (use VPN)Verify iOS ATT dialog appearsTest all ad formats load and displayCheck ads don't block UI elementsReview AdMob policies complianceAdd privacy policy to app/store listing
📜 License
MIT License - Feel free to use in any project.
🙏 Credits
Built with:
- google_mobile_ads - Official Google Mobile Ads SDK
- Flutter - Google's UI toolkit
📞 Support
For issues or questions:
- Check AdMob Help Center
- Review google_mobile_ads documentation
- See Flutter AdMob samples
Happy Monetizing! 💰