smart_google_ads

Smart Google AdMob Integration for Flutter

smart_google_ads helps Flutter apps manage Google Mobile Ads on Android and iOS through one consistent API. It supports adaptive banner, interstitial, rewarded, native, and app-open ad formats, with centralized initialization, ad preloading, lifecycle handling, optional Firebase Remote Config, and UMP consent support.

Demo

<video controls width="720" src="https://raw.githubusercontent.com/Hx-313/videos/main/smart-google-ads-demo.mp4"

Your browser does not support embedded videos.

▶ Watch the smart_google_ads demo

Step 1 — Install the package

flutter pub add smart_google_ads

What the package handles

smart_google_ads handles The host app still configures
Initializes the Mobile Ads SDK and manages ad loading, preloading, and lifecycle. AdMob app IDs in Android's manifest and iOS's Info.plist, plus Google Mobile Ads platform build settings.
Provides widgets and APIs for banner, interstitial, rewarded, native, and app-open ads. Production ad-unit IDs supplied through the package configuration.
Optionally integrates UMP consent and Firebase Remote Config. Firebase project files and initialization for Remote Config, plus app-owned mediation and privacy settings.

Defaults at a glance

Behavior Default
Ad formats Types with an ID for this platform are enabled; native is opt-in.
Native ads Disabled until you set nativeEnabled: true.
Preloading Enabled for configured and enabled ad formats.
Counted interstitials Threshold defaults to three visits; an ad shows only when ready.
Firebase Remote Config Off when remoteConfig is omitted; local bootstrap options are used.
UMP consent Omitted consent is disabled in debug and enabled in release. Explicit options override this.

If you enable Firebase Remote Config, also add Firebase Core:

flutter pub add firebase_core

If Remote Config is enabled, initialize Firebase before calling the bootstrap:

await Firebase.initializeApp();

The package does not read the host app's .env file and does not own Firebase initialization. IDs are passed explicitly, which makes the package portable and avoids hidden environment dependencies.

Steps 2–8 — Complete the host-app setup

Complete this setup once in the Flutter app that uses the package. The package can initialize the Mobile Ads SDK and load ad units, but only the host app can provide its platform configuration files. Follow the steps in order; Step 5A and Step 5B are alternatives, so choose only one Remote Config mode.

Step 2 — Create AdMob app IDs and ad-unit IDs

In AdMob, create an app for every platform you support and create the ad units you will use. There are two different kinds of IDs:

  • The AdMob app ID contains a tilde (~) and is placed in Android's manifest and iOS's Info.plist.
  • An ad-unit ID contains a slash (/) and is passed to PlatformAdIds in the Dart bootstrap configuration.

Do not put an ad-unit ID in the manifest or an app ID in PlatformAdIds.

Step 3 — Configure Android Gradle and manifest

The current dependencies require an Android app with minSdk 24 or higher. Use compileSdk 36 or higher. In android/app/build.gradle.kts (new Flutter projects), make sure the values are at least:

android {
    compileSdk = 36

    defaultConfig {
        minSdk = 24
    }
}

For an older Groovy project, the equivalent is:

android {
    compileSdkVersion 36

    defaultConfig {
        minSdkVersion 24
    }
}

If your project already has higher values, keep the higher values. Do not add another Google Mobile Ads Gradle dependency manually; google_mobile_ads provides it through the Flutter plugin.

Android GMA Next-Gen SDK

google_mobile_ads 9.1.0 can select Google's Android Next-Gen SDK through the USE_NEXT_GEN_SDK Dart define. Supply the define on every Android run or build command, including release commands. It is not an iOS setting and can be stored in an IDE launch configuration:

flutter run --dart-define=USE_NEXT_GEN_SDK=true
flutter build apk --dart-define=USE_NEXT_GEN_SDK=true
flutter build appbundle --dart-define=USE_NEXT_GEN_SDK=true

This package uses the Flutter plugin's native source selection. Do not add play-services-ads, play-services-ads-lite, or the Next-Gen SDK manually.

Next-Gen does not support mediation, AdMob mediation, or other mediation platforms. If the host app has mediation adapters, keep using the legacy SDK build until those adapters are removed. For a host Gradle project that needs to exclude legacy artifacts while resolving a Next-Gen-compatible dependency graph, use the matching syntax for its build file:

configurations.configureEach {
    exclude(group = "com.google.android.gms", module = "play-services-ads")
    exclude(group = "com.google.android.gms", module = "play-services-ads-lite")
}
configurations.configureEach {
    exclude group: 'com.google.android.gms', module: 'play-services-ads'
    exclude group: 'com.google.android.gms', module: 'play-services-ads-lite'
}

The package does not configure mediation adapters for the host app.

Open android/app/src/main/AndroidManifest.xml. Add the internet permission under <manifest> and the AdMob app ID inside <application>:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:label="your_app_name"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">

        <!-- Android AdMob APP ID: contains ~, not / -->
        <meta-data
            android:name="com.google.android.gms.ads.APPLICATION_ID"
            android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy" />

        <!-- Keep the rest of your existing application entries. -->
    </application>
</manifest>

Use Google's Android test app ID while testing: ca-app-pub-3940256099942544~3347511713.

Step 4 — Configure iOS Runner and Info.plist

The current Google Mobile Ads SDK requires iOS 13.0 or higher. In ios/Podfile, use:

platform :ios, '13.0'

Open ios/Runner/Info.plist and add the AdMob app ID inside the top-level <dict>:

<!-- iOS AdMob APP ID: contains ~, not / -->
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy</string>

No AppDelegate or Runner Dart code is required for Mobile Ads initialization; AdsBootstrap.init calls it for you. If your app separately requests App Tracking Transparency permission, add your own NSUserTrackingUsageDescription and request consent according to your privacy flow. This package does not request ATT permission automatically.

Step 5 — Create the Dart ad-unit configuration

Define the ad-unit IDs once in Dart. Use an ad-unit ID containing /, not the AdMob app ID containing ~. Each ad type is optional. This example uses Google's platform-specific test IDs in debug builds:

import 'package:smart_google_ads/smart_google_ads.dart';

const adUnitIds = AdsAdUnitIds(
  banner: PlatformAdIds(
    android: 'ca-app-pub-xxx/banner-android',
    ios: 'ca-app-pub-xxx/banner-ios',
    debugAndroid: 'ca-app-pub-3940256099942544/6300978111',
    debugIos: 'ca-app-pub-3940256099942544/2934735716',
  ),
  interstitial: PlatformAdIds(
    android: 'ca-app-pub-xxx/interstitial-android',
    ios: 'ca-app-pub-xxx/interstitial-ios',
    debugAndroid: 'ca-app-pub-3940256099942544/1033173712',
    debugIos: 'ca-app-pub-3940256099942544/4411468910',
  ),
  rewarded: PlatformAdIds(
    android: 'ca-app-pub-xxx/rewarded-android',
    ios: 'ca-app-pub-xxx/rewarded-ios',
    debugAndroid: 'ca-app-pub-3940256099942544/5224354917',
    debugIos: 'ca-app-pub-3940256099942544/1712485313',
  ),
);

Replace the xxx values with your production ad-unit IDs. The debug fields are used only in debug builds; release builds use the normal Android/iOS IDs.

Step 6A — Configure Firebase Remote Config (optional)

Skip this section when using local mode, which is the default.

From the host Flutter app's directory:

flutter pub add firebase_core
dart pub global activate flutterfire_cli
flutterfire configure

flutterfire configure creates lib/firebase_options.dart and configures the selected Android/iOS Firebase apps. Initialize Firebase before the ads bootstrap:

import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/widgets.dart';
import 'package:smart_google_ads/smart_google_ads.dart';

import 'firebase_options.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await Firebase.initializeApp(
    options: DefaultFirebaseOptions.currentPlatform,
  );

  await AdsBootstrap.init(
    adUnitIds: adUnitIds,
    remoteConfig: const AdsRemoteConfigOptions(),
    consent: const AdsConsentOptions(),
  );

  runApp(const MyApp());
}

The package owns Remote Config defaults, fetching, activation, and fallback. The host app owns the Firebase project and its GoogleService-Info.plist / google-services.json configuration generated by FlutterFire.

Step 6B — Use local controls without Remote Config

This is the simplest setup. Do not add Firebase for ads. Leave remoteConfig omitted, and control the package from the boolean and count arguments passed to AdsBootstrap.init.

await AdsBootstrap.init(
  adUnitIds: adUnitIds,
  adsEnabled: true,
  bannerEnabled: true,
  interstitialEnabled: true,
  interstitialAfter: 3,
  rewardedEnabled: true,
  nativeEnabled: false,
  appOpenEnabled: false,
  directInterstitialEnabled: false,
);

The local controls mean:

Argument true false
adsEnabled Allows every configured ad type. Turns off all ads.
bannerEnabled Allows banners when a banner ID exists. Disables banners.
interstitialEnabled Allows counted interstitials. Disables counted interstitials.
rewardedEnabled Allows rewarded ads. Disables rewarded ads.
nativeEnabled Allows native ads when a native ID exists. Disables native ads.
appOpenEnabled Allows app-open ads. Disables app-open ads.
directInterstitialEnabled Allows direct/splash interstitials. Disables direct interstitials.

An explicit true cannot create an ad without an ID. If an ID is missing for the current platform, that ad type remains unavailable. When a flag is omitted, most types automatically enable if an ID is available; native ads are opt-in and default to disabled.

interstitialAfter controls only the frequency-based APIs:

final ads = AdsService.instance;

ads.onScreenVisit();
// The third visit is eligible when interstitialAfter is 3 and an ad is ready.

ads.showBeforeNavigation(() {
  Navigator.of(context).push(nextRoute);
});

The ad must already be loaded. If it is not ready, navigation continues and the package keeps loading it in the background. showInterstitialNow() and showDirect() intentionally bypass the visit count. Set interstitialEnabled: false to disable counted interstitials, or adsEnabled: false to disable everything.

These local values are selected when the package starts. To change them for all users, update the code and publish an app update. Use Remote Config when you need to change the values without publishing a new app version.

The package includes an optional integration with Google's User Messaging Platform (UMP). It requests fresh consent information, shows the UMP form when required, and blocks ad loading until UMP says that ad requests are allowed. When consent is omitted, debug builds use const AdsConsentOptions.disabled() so Google test ads can be exercised without a UMP gate, while release builds use const AdsConsentOptions() and enforce UMP. An explicit consent value always overrides this default.

This environment-aware default is a breaking policy change in version 0.2.0. Apps that need UMP during debug can pass explicit debugGeography and testDeviceIds values as shown below.

Before release, create and publish the applicable message in AdMob's Privacy & messaging area. The package displays Google's configured UMP message; it cannot create or publish that message for the host app.

For a new app, use the consent-aware bootstrap and apply only the restrictions that match your verified audience and legal/privacy decisions:

await AdsBootstrap.init(
  adUnitIds: adUnitIds,
  consent: AdsConsentOptions(
    // Optional: development-only UMP diagnostics.
    // debugGeography: AdsConsentDebugGeography.eea,
    // testDeviceIds: ['YOUR_UMP_TEST_DEVICE_ID'],
    onError: (error) {
      dPrint('Consent error: $error');
    },
  ),
  policy: const AdsPolicyOptions(
    maxAdContentRating: AdsMaxAdContentRating.pg,
    // Set child or teen treatment only when it is accurate and authorized.
    ageTreatment: AdsAgeTreatment.unspecified,
  ),
);

AdsPolicyOptions applies Google's global request configuration to future ads. It uses the current ageRestrictedTreatment API and does not use deprecated child-directed flags. testDeviceIds and debugGeography are for development only; remove them from release configuration.

If Google says a privacy-options entry point is required, expose it from a visible privacy/settings screen:

FutureBuilder<bool>(
  future: AdsBootstrap.isPrivacyOptionsRequired(),
  builder: (context, snapshot) {
    if (snapshot.data != true) return const SizedBox.shrink();
    return TextButton(
      onPressed: () {
        AdsBootstrap.showPrivacyOptions();
      },
      child: const Text('Privacy options'),
    );
  },
);

AdsBootstrap.showPrivacyOptions() refreshes the package's ad gate after the user saves a new choice. Consent errors are reported through onError; a previously stored valid decision can continue to work offline, while an unknown decision blocks ad requests.

The host app is still responsible for its privacy policy, age/audience classification, regional publisher requirements, Google consent-message configuration, and any iOS App Tracking Transparency (ATT) disclosure and permission flow. The package does not silently request ATT, make legal decisions, or enable Firebase/Google Consent Mode on the host's behalf.

Step 8 — Final setup checklist

  • flutter pub add smart_google_ads completed.
  • AdMob app ID added to Android AndroidManifest.xml.
  • AdMob app ID added to iOS Runner/Info.plist.
  • Android minSdk is at least 24 and iOS deployment target is at least 13.0.
  • PlatformAdIds contains ad-unit IDs, not app IDs.
  • Google test IDs are used during development.
  • Firebase is initialized before remoteConfig: AdsRemoteConfigOptions().
  • Release consent uses the UMP flow (the omitted default or an explicit consent: AdsConsentOptions()), unless the host app intentionally owns an equivalent consent flow.
  • USE_NEXT_GEN_SDK=true is present on every Android Next-Gen build/run.
  • No unsupported mediation adapter is enabled for a Next-Gen build.
  • policy: AdsPolicyOptions(...) matches the verified audience and content rating requirements.
  • A visible privacy-options entry point is provided when UMP requires it.
  • AdsBootstrap.init runs before the first ad widget is built.

Step 9 — Remote Config controls

Remote Config is optional. The package first builds a local fallback from the IDs and local options, then overlays remote values. If Firebase, the network, or a key is unavailable, the fallback remains active.

The standard keys are:

Package key Default Remote Config key Type
AdRemoteKeys.adsEnabled ads_enabled bool
AdRemoteKeys.banner isBannerShow bool
AdRemoteKeys.interstitial isIntersitialShow bool
AdRemoteKeys.rewarded rewardedEnabled bool
AdRemoteKeys.native isNativeShow bool
AdRemoteKeys.appOpen isAppopenShow bool
AdRemoteKeys.directInterstitial isSplashIntersitialShow bool
AdRemoteKeys.adsCount adsCount int

For example, Remote Config values of ads_enabled = true, isBannerShow = true, isIntersitialShow = true, and adsCount = 3 enable banners, enable counted interstitials, and show an interstitial opportunity after three visits. Setting any boolean to false disables that feature; setting ads_enabled to false disables all ad types. These dashboard values are only used when Remote Config is enabled; otherwise use the local bootstrap arguments described in Step 6B.

For a project with different names, use the typed options:

await AdsBootstrap.init(
  adUnitIds: adUnitIds,
  remoteConfig: AdsRemoteConfigOptions(
    keys: {
      AdRemoteKeys.adsEnabled: 'ads_enabled',
      AdRemoteKeys.banner: 'show_banner',
      AdRemoteKeys.interstitial: 'show_interstitial',
      AdRemoteKeys.adsCount: 'interstitial_frequency',
    },
    onFetched: (remoteConfig) {
      // Read app-specific Remote Config values here if needed.
    },
  ),
);

The host app must have a configured Firebase project. Remote Config controls whether an already-configured ad type is enabled; it cannot create a missing ad unit ID.

Step 10 — Show ads

final ads = AdsService.instance;

// Count a visit and show only when an already-loaded interstitial is ready.
ads.onScreenVisit();

// For navigation-gated flows; navigation always continues if no ad is ready.
ads.showBeforeNavigation(() => Navigator.of(context).push(nextRoute));

// Immediate interstitial; returns false when unavailable.
final shown = await ads.showInterstitialNow();

// Rewarded ad; premium users receive the reward callback without an ad.
await ads.showRewarded(
  onUserEarnedReward: () async => grantReward(),
);

// Splash/direct interstitial; navigation resumes when it is dismissed or skipped.
ads.showDirect(onResumeNavigation: () => openHome());

Step 11 — Add smart widgets

Use the package widgets instead of constructing BannerAd or AdWidget directly. They check initialization, premium status, Remote Config, IDs, loading state, and banner visibility for you:

const SmartBannerAdWidget(
  padding: EdgeInsets.symmetric(vertical: 8),
  collapsible: true,
)

Native ads are opt-in because apps commonly need different placements:

const SmartNativeAdWidget(size: NativeTemplateSize.small)

SmartBannerAdWidget uses an anchored adaptive banner and is intended for a stable top or bottom placement. For a scrolling feed, use the underlying google_mobile_ads inline adaptive banner APIs instead of placing an anchored banner inside the feed.

Step 12 — Use SmartDialog for modal UI

Do not call Flutter's showDialog, showModalBottomSheet, or showCupertinoDialog directly when a banner can be on screen. Use SmartDialog (or the BuildContext extensions) so the banner is hidden while the modal is open and app-open ads are blocked during the modal flow. State is restored even if the modal throws or is dismissed:

await SmartDialog.showMaterialDialog<void>(
  context: context,
  dialog: AlertDialog(
    title: const Text('Remove ads'),
    content: const Text('Upgrade to continue without advertising.'),
    actions: [
      TextButton(
        onPressed: () => Navigator.pop(context),
        child: const Text('Close'),
      ),
    ],
  ),
);

await context.showSmartBottomSheet<void>(
  builder: (context) => const SizedBox(
    height: 240,
    child: Center(child: Text('Ad-aware bottom sheet')),
  ),
);

For camera, file picker, permission, share sheet, or another flow that can temporarily background the app, use the app-open suppression API:

final file = await AdsService.instance.runWithoutAppOpen(
  () => filePicker.pickFiles(),
  reason: 'file_picker',
);

Step 13 — Protect app-open flows

Use skipNextAppOpen() for one short external transition, or blockAppOpen(reason)/unblockAppOpen(reason) for a named long-running flow. runWithoutAppOpen is preferred because it guarantees cleanup and has a safety timeout.

Step 14 — Release checklist

  • Use Google's test IDs during development and replace them with production IDs before release.
  • Configure the AdMob application ID in the host app's Android/iOS setup.
  • Initialize Firebase before using Remote Config.
  • For Android Next-Gen, pass --dart-define=USE_NEXT_GEN_SDK=true to every run, APK, and app-bundle command.
  • Do not enable unsupported mediation adapters in a Next-Gen build.
  • Never show an interstitial immediately on app launch or on every tap.
  • Keep the isProUser callback cheap and synchronous; it may be called often.
  • Reset the package only in tests or when intentionally rebuilding its global singleton: AdsService.reset().

Next-Gen smoke verification

For a clean package checkout, generate the example runner and inspect its debug dependency graph:

cd example
flutter create .
flutter pub get
flutter build apk --debug --dart-define=USE_NEXT_GEN_SDK=true
cd android
.\gradlew.bat app:dependencies --configuration debugRuntimeClasspath | Select-String "ads-mobile-sdk|play-services-ads"

The graph should contain com.google.android.libraries.ads.mobile.sdk:ads-mobile-sdk and, with no mediation configured, should not contain the legacy com.google.android.gms:play-services-ads artifact selected by the plugin. Keep Google test IDs and UMP debug settings limited to development, verify the UMP privacy message and privacy-options entry point, and keep the app ID in the generated Android manifest and iOS Info.plist.

Official setup references:

Troubleshooting

  • No ad appears: confirm the AdMob app ID is in the Android/iOS host configuration, the matching ad-unit ID is in PlatformAdIds, and AdsBootstrap.init runs before ad widgets are built. Use Google's test IDs during development. Consent, connectivity, and ad inventory also affect availability; an ad is not guaranteed to fill.
  • A native ad stays hidden: native ads are opt-in. Set nativeEnabled: true and include a native ad-unit ID for the current platform.
  • Ads are blocked in a release build: when consent options are omitted, release builds use UMP. Publish the applicable privacy message in AdMob and check that UMP allows ad requests.
  • Remote Config values are not applied: pass AdsRemoteConfigOptions and initialize Firebase before AdsBootstrap.init. Missing or unavailable values fall back to the local configuration.

Package resources

License

This package is available under the MIT license. See LICENSE.