ad_flow 1.0.0+2
ad_flow: ^1.0.0+2 copied to clipboard
Easy AdMob integration for Flutter. Banner, interstitial, native & app open ads with GDPR/ATT consent management.
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 |
| 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 |
| Remove Ads | ✅ | Built-in IAP support to disable ads |
| Auto Preloading | ✅ | Ads ready when you need them |
| Retry Logic | ✅ | Exponential backoff on failures |
📦 Installation #
1. Add Dependency #
# pubspec.yaml
dependencies:
ad_flow: ^1.0.0
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 #
AdService 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 AdService.instance to show ads.
┌─────────────────────────────────────────────────────────────┐
│ YOUR APP │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Page 1 │ │ Page 2 │ │ Page 3 │ │
│ │ (Splash) │ │ (Home) │ │ (Details) │ │
│ │ │ │ │ │ │ │
│ │ initialize() │───▶│ showBanner() │───▶│ showBanner() │ │
│ │ ✅ │ │ ✅ │ │ ✅ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │ │ │ │
│ └────────────────────┴────────────────────┘ │
│ │ │
│ ┌─────────▼─────────┐ │
│ │ AdService │ │
│ │ (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 'ads/ads.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize AdMob with consent handling (ONCE!)
await AdService.instance.initialize(
onComplete: (canRequestAds) {
debugPrint('Ads ready: $canRequestAds');
},
preloadInterstitial: true, // Preload interstitial
preloadAppOpen: true, // Preload app open ad
enableAppOpenOnForeground: true, // Show ad on app resume
);
runApp(const MyApp());
}
📱 Usage Examples #
Banner Ads (Easiest Way) #
import 'ads/ads.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(() {}),
);
Interstitial Ads #
// Show interstitial (auto-preloaded on init)
await AdService.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 (AdService.instance.interstitial.isLoaded) {
AdService.instance.interstitial.showAd();
}
Interstitial with Frequency Control #
int _actionCount = 0;
void _onUserAction() {
_actionCount++;
// Show interstitial every 5 actions
if (_actionCount % 5 == 0) {
if (AdService.instance.interstitial.isLoaded) {
AdService.instance.interstitial.showAd();
}
}
}
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 (AdService.instance.appOpen.isAdAvailable) {
await AdService.instance.appOpen.showAdIfAvailable(
onAdDismissed: () {
// App resumed
},
);
}
Privacy Settings Button #
// Check if user needs privacy options (GDPR regions)
if (AdService.instance.isPrivacyOptionsRequired) {
IconButton(
icon: const Icon(Icons.privacy_tip),
onPressed: () {
AdService.instance.showPrivacyOptions(
onComplete: () {
// User updated privacy settings
},
);
},
);
}
Ad Inspector (Debug Mode) #
// Open the Ad Inspector for debugging
AdService.instance.openAdInspector();
Remove Ads (In-App Purchase) #
Built-in support for "Remove Ads" purchases:
// After successful IAP purchase
await AdService.instance.disableAds();
// All ad widgets automatically hide!
// EasyBannerAd, EasyNativeAd, etc. respect this setting.
// Check if ads are enabled
if (AdService.instance.isAdsEnabled) {
// Show ads
}
// Re-enable ads (e.g., restore purchase failed)
await AdService.instance.enableAds();
// Reactive UI with StreamBuilder
StreamBuilder<bool>(
stream: AdService.instance.adsEnabledStream,
builder: (context, snapshot) {
final adsEnabled = snapshot.data ?? true;
if (!adsEnabled) return const SizedBox.shrink();
return const EasyBannerAd();
},
)
📂 Package Structure #
lib/ads/
├── ads.dart # Barrel export (import this)
├── ad_config.dart # Configuration & ad unit IDs
├── ad_service.dart # Main 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
├── interstitial_ad_manager.dart # Interstitial 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 #
Ad Unit IDs #
Edit lib/ads/ad_config.dart to set your production ad unit IDs:
class AdConfig {
// Replace with your production IDs from AdMob console
static String get bannerAdUnitId {
if (Platform.isAndroid) {
return 'ca-app-pub-YOUR_ID/BANNER_ID';
} else if (Platform.isIOS) {
return 'ca-app-pub-YOUR_ID/BANNER_ID';
}
throw UnsupportedError('Unsupported platform');
}
static String get interstitialAdUnitId {
if (Platform.isAndroid) {
return 'ca-app-pub-YOUR_ID/INTERSTITIAL_ID';
} else if (Platform.isIOS) {
return 'ca-app-pub-YOUR_ID/INTERSTITIAL_ID';
}
throw UnsupportedError('Unsupported platform');
}
static String get appOpenAdUnitId {
if (Platform.isAndroid) {
return 'ca-app-pub-YOUR_ID/APP_OPEN_ID';
} else if (Platform.isIOS) {
return 'ca-app-pub-YOUR_ID/APP_OPEN_ID';
}
throw UnsupportedError('Unsupported platform');
}
}
Behavior Settings #
// In ad_config.dart
/// Cache app open ads for max 4 hours (Google's recommendation)
static const Duration appOpenAdMaxCacheDuration = Duration(hours: 4);
/// Minimum time between interstitial ads
static const Duration minInterstitialInterval = Duration(seconds: 60);
/// Number of retry attempts for failed ad loads
static const int maxLoadRetries = 3;
/// Delay between retries
static const Duration retryDelay = Duration(seconds: 5);
Test Device IDs #
Add your test device ID to avoid invalid impressions during development:
// In ad_config.dart
static const List<String> testDeviceIds = [
'YOUR_DEVICE_HASHED_ID', // From logcat/console
];
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((_) {
AdService.instance.initializeWithExplainer(
context: context,
onComplete: (canRequestAds) {
debugPrint('Ads ready: $canRequestAds');
},
);
});
}
}
// Option 2: Standard initialization (consent popups appear immediately)
await AdService.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
AdService.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');
AdService.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 #
// Recommended: Every 3-5 user actions or natural breaks
static const Duration minInterstitialInterval = Duration(seconds: 60);
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 (optional, advanced)
- ✅ Target appropriate content rating
- ✅ Maintain high user engagement
🔍 API Reference #
AdService #
// Singleton instance
AdService.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
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
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
double? height, // Ad height
Widget? placeholder, // Loading placeholder
})
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 #
- ❌ Replace test ad unit IDs with production IDs
- ❌ Remove test device IDs
- ❌ 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! 💰