ad_flow 1.3.11
ad_flow: ^1.3.11 copied to clipboard
Easy AdMob integration for Flutter with banner, interstitial, rewarded, native & app open ads, plus built-in GDPR/ATT consent management and mediation support.
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 #
- ❌ Use
AdFlowConfigwith your production ad unit IDs - ❌ Remove
testDeviceIdsor leave empty - ❌ Set
enableConsentDebug: false - ❌ Test on real devices
- ❌ Test consent flow in GDPR region (use VPN)
- ❌ Verify iOS ATT dialog appears
- ❌ Test all ad formats load and display
- ❌ Check ads don't block UI elements
- ❌ Review AdMob policies compliance
- ❌ Add 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! 💰