The missing high-level AdMob wrapper for Flutter.
Stop writing hundreds of lines of boilerplate for every ad format.
flutter_admob_kit gives you banner, interstitial, rewarded, and app-open
ads with a clean API, automatic UMP consent handling, and zero dependency
on any state management framework.
- One-line initialization --
await adManager.initialize()sets up AdMob SDK + UMP consent in one call - Widget API + Imperative API -- Drop-in widgets for declarative UI, or call methods from business logic
- Automatic test/production switching -- Debug builds use Google's test ad IDs; release builds use yours
- UMP consent flow -- GDPR/CCPA consent handled automatically; manual trigger available for settings screens
- Retry with exponential backoff -- Failed loads retry automatically (configurable attempts and delay)
- Frequency capping -- Prevent interstitial ad fatigue with configurable cooldown
- App-open ads -- Show ads on foreground resume with optional cold-start skip
- Event streaming -- Forward ad events to Firebase Analytics or any analytics service
- Fully testable -- Ships with
FakeAdManagerfor unit and widget tests - State-management agnostic -- Works with Provider, Riverpod, Bloc, GetX, or plain Flutter
Add to your pubspec.yaml:
dependencies:
flutter_admob_kit: ^0.1.0
google_mobile_ads: ^5.0.0 # peer dependency -- you manage the versionAndroid -- Add your AdMob App ID to AndroidManifest.xml:
<manifest>
<application>
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy"/>
</application>
</manifest>iOS -- Add your AdMob App ID to Info.plist:
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy</string>import 'package:flutter/material.dart';
import 'package:flutter_admob_kit/flutter_admob_kit.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
final adManager = AdManager(
config: AdConfig(
bannerId: AdUnitId(
android: 'ca-app-pub-xxxxx/banner-android',
ios: 'ca-app-pub-xxxxx/banner-ios',
),
interstitialId: AdUnitId(
android: 'ca-app-pub-xxxxx/interstitial-android',
ios: 'ca-app-pub-xxxxx/interstitial-ios',
),
rewardedId: AdUnitId(
android: 'ca-app-pub-xxxxx/rewarded-android',
ios: 'ca-app-pub-xxxxx/rewarded-ios',
),
testDeviceIds: ['YOUR_TEST_DEVICE_ID'],
),
);
await adManager.initialize();
runApp(MyApp(adManager: adManager));
}// Drop into any widget tree -- that's it.
BannerAdWidget(manager: adManager)
// Or use an adaptive banner that fills the available width:
BannerAdWidget.adaptive(manager: adManager)Widget API -- wrap any widget to show an ad on tap:
InterstitialTrigger(
manager: adManager,
onComplete: () => Navigator.push(context, nextRoute),
onSkipped: () => Navigator.push(context, nextRoute),
child: ElevatedButton(
onPressed: null, // controlled by InterstitialTrigger
child: Text('Next Level'),
),
)Imperative API -- call from business logic:
await adManager.loadInterstitial();
// Later, when ready to show:
final shown = await adManager.showInterstitial();
if (!shown) {
// Ad wasn't ready or frequency cap active -- proceed normally
}await adManager.loadRewarded();
final reward = await adManager.showRewarded();
if (reward != null) {
gameState.addCoins(reward.amount);
}Consent is handled automatically during initialize(). For a manual
settings-screen trigger:
final result = await adManager.consent.requestConsent();
switch (result) {
case ConsentResult.obtained:
print('Personalized ads allowed');
case ConsentResult.denied:
print('Non-personalized ads only');
case ConsentResult.error:
print('Consent check failed, using non-personalized');
}final adManager = AdManager(
config: AdConfig(
bannerId: AdUnitId(android: '...', ios: '...'),
appOpenId: AdUnitId(
android: 'ca-app-pub-xxxxx/appopen-android',
ios: 'ca-app-pub-xxxxx/appopen-ios',
),
skipColdStartAppOpen: true, // skip on first launch
),
);
await adManager.initialize();
adManager.enableAppOpenAd(); // shows on foreground resumeadManager.onAdEvent.listen((event) {
switch (event.type) {
case AdEventType.shown:
analytics.logEvent(name: 'ad_shown');
case AdEventType.rewardEarned:
print('Earned ${event.reward!.amount} ${event.reward!.type}');
case AdEventType.paidEvent:
final pv = event.paidValue!;
analytics.logRevenue(pv.valueMicros, pv.currencyCode);
default:
break;
}
});The package ships with FakeAdManager so your tests never touch real ads:
import 'package:flutter_admob_kit/flutter_admob_kit.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
test('reward is granted after watching ad', () async {
final fake = FakeAdManager();
fake.nextRewardedResult = const AdReward(amount: 50, type: 'gems');
await fake.initialize();
await fake.loadRewarded();
final reward = await fake.showRewarded();
expect(reward?.amount, 50);
expect(reward?.type, 'gems');
});
}| Parameter | Default | Description |
|---|---|---|
bannerId |
required | Banner ad unit ID (Android + iOS) |
interstitialId |
null |
Interstitial ad unit ID (null = disabled) |
rewardedId |
null |
Rewarded ad unit ID (null = disabled) |
appOpenId |
null |
App-open ad unit ID (null = disabled) |
testDeviceIds |
[] |
Physical device IDs for test ads |
frequencyCapSeconds |
60 |
Min seconds between interstitial impressions |
maxRetryCount |
3 |
Retry attempts on load failure |
retryBaseDelayMs |
1000 |
Base delay for exponential backoff (ms) |
loadTimeoutSeconds |
30 |
Timeout per load attempt (seconds) |
skipColdStartAppOpen |
true |
Skip app-open ad on first launch |
| Platform | Minimum |
|---|---|
| Flutter | 3.22+ |
| Dart | 3.4+ |
| Android | API 21+ |
| iOS | 14.0+ |
MIT -- see LICENSE.