vie_app_ads_manager

A lightweight, opinionated ads manager for Flutter that unifies AdMob (Google Mobile Ads) and AppLovin MAX into a single, simple API.

Features

  • Full Ad Format Support: Interstitial, Rewarded, Rewarded-Interstitial (AdMob), App Open, Banner, Native
  • Smart Fallback System: Automatic fallback from AdMob to AppLovin MAX when configured
  • Remote Config Driven Providers: Select AdMob/MAX and enable flags via Firebase Remote Config
  • Widget-Based Ads: Banner and Native ad widgets that auto-select provider via RC
  • Event Streaming: Real-time ad lifecycle events for UI state management
  • Test Mode Support: Built-in AppLovin MAX mediation debugger and AdMob test ads
  • Robust Error Handling: Comprehensive logging and state management

Works with: google_mobile_ads, applovin_max.

Installation

Add to your pubspec.yaml:

dependencies:
  vie_app_ads_manager: ^2.0.2

Then run:

flutter pub get

Platform setup

Follow the official setup for each SDK your app will use and apply the additional configuration below.

Android configuration

  1. Project-level Gradle (android/build.gradle)

    Add the AppLovin repository and configure dependencies with version alignment:

    buildscript {
        repositories {
            google()
            mavenCentral()
            maven { url = uri("https://artifacts.applovin.com/android") }
        }
        dependencies {
            classpath 'com.android.tools.build:gradle:8.1.2' // Use your current version
            classpath 'com.google.gms:google-services:4.3.15' // Add this line for Google Services
        }
    }
    
    allprojects {
        repositories {
            google()
            mavenCentral()
            maven { url = uri("https://artifacts.applovin.com/android") }
        }
        configurations.all {
            resolutionStrategy {
                force(
                        "com.google.android.gms:play-services-ads:24.7.0",
                        "com.applovin.mediation:google-adapter:24.7.0.0"
                )
            }
        }
    }
    

    Note: Adapt versions to the latest recommended by google_mobile_ads and applovin_max packages.

  2. App-level Gradle (android/app/build.gradle)

    Add the mediation adapter dependencies to the dependencies block:

    dependencies {
        // Align Google Mobile Ads SDK with AppLovin Google adapter
        implementation("com.google.android.gms:play-services-ads:24.7.0")
        implementation("com.applovin.mediation:google-adapter:24.7.0.0")
    }
    
  3. AndroidManifest (android/app/src/main/AndroidManifest.xml)

    Add required permissions, AdMob application ID, and AppLovin MAX activities:

    <manifest xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
    
        <uses-permission android:name="android.permission.INTERNET" />
        <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    
        <application
            android:name="${applicationName}"
            android:icon="@mipmap/launcher_icon"
            android:label="YOUR_APP_NAME">
               
            <!-- Your main activity configuration -->
            <activity
                android:name=".MainActivity"
                android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
                android:exported="true"
                android:hardwareAccelerated="true"
                android:launchMode="singleTask"
                android:taskAffinity="${applicationId}"
                android:theme="@style/LaunchTheme"
                android:windowSoftInputMode="adjustResize">
                <intent-filter>
                    <action android:name="android.intent.action.MAIN" />
                    <category android:name="android.intent.category.LAUNCHER" />
                </intent-filter>
            </activity>
    
            <!-- AdMob App ID required for Google Mobile Ads SDK initialization -->
            <meta-data
                android:name="com.google.android.gms.ads.APPLICATION_ID"
                android:value="YOUR_ADMOB_APP_ID_ANDROID" />
                   
            <!-- AppLovin MAX Ad Activities - Force them to use our task -->
            <activity
                android:name="com.applovin.adview.AppLovinInterstitialActivity"
                android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"
                android:excludeFromRecents="true"
                android:taskAffinity="${applicationId}"
                tools:replace="android:configChanges,android:taskAffinity" />
            <activity
                android:name="com.applovin.adview.AppLovinFullscreenActivity"
                android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"
                android:excludeFromRecents="true"
                android:taskAffinity="${applicationId}"
                tools:replace="android:configChanges,android:taskAffinity" />
            <!-- Optional: For webview-based ads or browsers -->
            <activity
                android:name="com.applovin.sdk.AppLovinWebViewActivity"
                android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"
                android:excludeFromRecents="true"
                android:taskAffinity="${applicationId}"
                tools:replace="android:configChanges,android:taskAffinity" />
            <!-- Optional: If using the mediation debugger -->
            <!-- REMOVE THIS IN PRODUCTION -->
    <!--        <activity-->
    <!--            android:name="com.applovin.mediation.MaxDebuggerActivity"-->
    <!--            android:configChanges="keyboard|keyboardHidden|orientation|screenLayout|uiMode|screenSize|smallestScreenSize"-->
    <!--            android:excludeFromRecents="true"-->
    <!--            android:taskAffinity="${applicationId}"-->
    <!--            tools:replace="android:configChanges,android:taskAffinity" />-->
               
            <meta-data
                android:name="flutterEmbedding"
                android:value="2" />
        </application>
           
        <queries>
            <intent>
                <action android:name="android.intent.action.PROCESS_TEXT" />
                <data android:mimeType="text/plain" />
            </intent>
        </queries>
    </manifest>
    

    For AdMob Mediation Update your gradle settings (Android only) Add the following lines to your settings.gradle file, so you can use the plugin's Android APIs:

def flutterSdkPath = {
  def properties = new Properties()
  file("local.properties").withInputStream { properties.load(it) }
  def flutterSdkPath = properties.getProperty("flutter.sdk")
  assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
  return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")

Important:

  • Replace YOUR_ADMOB_APP_ID_ANDROID with your real AdMob app ID
  • Replace YOUR_APP_NAME with your app name
  • The AppLovin MAX activities ensure ads stay within your app's task
  • Uncomment the debugger activity only during development
  • The <queries> tag is required for some ad networks to work properly

iOS configuration

This package works on iOS as well as Android. The Dart implementation already handles iOS IDs (for example appIdIos) and requests App Tracking Transparency consent on iOS. You only need to complete the platform-specific Xcode setup below.

  1. Info.plist AdMob app ID & tracking permission

    In your iOS Runner target (ios/Runner/Info.plist), add your AdMob app ID and a tracking description (required by App Tracking Transparency and recommended by Google):

    <key>GADApplicationIdentifier</key>
    <string>YOUR_ADMOB_APP_ID_IOS</string>
    
    <key>NSUserTrackingUsageDescription</key>
    <string>This identifier will be used to deliver personalized ads to you.</string>
    
  2. AppLovin MAX

    • On iOS, the Flutter applovin_max plugin is initialized entirely in Dart via MaxIds.sdkKey and AppLovinMAX.initialize(...), which this package already calls.
    • For a basic integration, you do not need additional Info.plist keys.
    • For advanced configuration (e.g. additional SKAdNetworkItems entries for MAX and mediated networks), follow the official AppLovin MAX iOS docs:
  3. Pods & adapters

    • The necessary iOS SDKs and mediation adapters are pulled in automatically via CocoaPods from the google_mobile_ads and applovin_max Flutter plugins.
    • After adding this package and the required plugins to your pubspec.yaml, run pod install inside the ios directory (or flutter build ios / flutter run, which will trigger CocoaPods).
    • You usually do not need to manually edit Podfile or add adapter pods yourself unless the upstream plugin documentation explicitly tells you to.
  4. Adjust (iOS)

    • This package initializes Adjust from Dart using the adjustAppToken you pass into AdsManager.instance.initialize(...).
    • The native iOS Adjust SDK and required pods are integrated via the official adjust_sdk Flutter plugin; you normally do not need extra manual Podfile changes.
    • For advanced features (e.g., SKAdNetworkItems, deep links, or special attribution settings), follow the official Adjust Flutter documentation and add any additional Info.plist keys they recommend for iOS.

Make sure you use your own ad unit IDs in production. Use test IDs during development.

Getting started

Import the package:

import 'package:vie_app_ads_manager/ads_manager.dart';

Initialize as early as possible (e.g., in main or first page load). You must initialize Firebase and the provided AnalyticsManager to fetch Remote Config before initializing AdsManager:

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: const Home());
  }
}

class Home extends StatefulWidget {
  const Home({super.key});
  @override
  State<Home> createState() => _HomeState();
}

class _HomeState extends State<Home> {
  bool _initializing = true;

  @override
  void initState() {
    super.initState();
    _initAds();
  }

  Future<void> _initAds() async {
    // 1) Initialize Analytics + Remote Config
    await AnalyticsManager.instance.init(
      options: /* Your DefaultFirebaseOptions.currentPlatform */, 
      minimumFetchInterval: Duration.zero,
    );

    // 2) Provide your AdMob and (optionally) AppLovin MAX IDs
    final admobIds = AdmobIds(
      appIdAndroid: 'YOUR_ADMOB_APP_ID_ANDROID',
      appIdIos: 'YOUR_ADMOB_APP_ID_IOS',
      bannerId: 'YOUR_ADMOB_BANNER_ID',
      interstitialId: 'YOUR_ADMOB_INTERSTITIAL_ID',
      rewardedId: 'YOUR_ADMOB_REWARDED_ID',
      rewardedInterstitialId: 'YOUR_ADMOB_REWARDED_INTERSTITIAL_ID',
      nativeId: 'YOUR_ADMOB_NATIVE_ID',
      appOpenId: 'YOUR_ADMOB_APPOPEN_ID',
    );

    // Optional: provide AppLovin MAX IDs to enable fallback or to force MAX
    final maxIds = MaxIds(
      sdkKey: 'YOUR_MAX_SDK_KEY',
      bannerId: 'YOUR_MAX_BANNER_ID',
      interstitialId: 'YOUR_MAX_INTERSTITIAL_ID',
      rewardedId: 'YOUR_MAX_REWARDED_ID',
      nativeId: 'YOUR_MAX_NATIVE_ID',
      appOpenId: 'YOUR_MAX_APPOPEN_ID',
    );

    // 3) Initialize AdsManager (it will read provider flags from Remote Config)
    await AdsManager.instance.initialize(
      admobIds: admobIds,
      maxIds: maxIds,
      adjustAppToken: 'YOUR_ADJUST_APP_TOKEN',
    );
    if (!mounted) return;
    setState(() => _initializing = false);
  }
}

## Remote Config Provider Control

Provider selection and feature flags are controlled via Firebase Remote Config (RC). Configure these keys in RC:

- `show_banner` (bool): enable/disable banner widget
- `native_ad_enabled` (bool): enable/disable native widget
- `show_app_open` (bool): enable/disable app open button/flow
- `banner_provider` (int): 0=off, 1=AdMob only, 2=MAX only
- `interstitial_provider` (int): 0=off, 1=AdMob only, 2=MAX only
- `reward_provider` (int): 0=off, 1=AdMob only, 2=MAX only
- `app_open_provider` (int): 0=off, 1=AdMob only, 2=MAX only
- `native_ad_provider` (int): 0=off, 1=AdMob only, 2=MAX only

AdsManager reads these values during `initialize()` via `AnalyticsManager`.

### How selection works

- `0` → No-Op: show methods return false; widgets render nothing
- `1` → AdMob only (no MAX fallback)
- `2` → MAX only
- Otherwise: default logic attempts AdMob first, with MAX as fallback when available

### Interstitial
```dart
// Provider is selected via RC
final shown = await AdsManager.instance.showInterstitial();

Rewarded

final shown = await AdsManager.instance.showRewarded(
  onEarnedReward: (amount) {
    // grant reward
  },
);

App Open

final shown = await AdsManager.instance.showAppOpen();

Widgets

A banner widget that auto-selects provider via Remote Config, with automatic fallback when allowed.

// Default behavior (provider sourced from RC)
const BannerAdManager()

// Custom size
const BannerAdManager(adSize: AdSize.largeBanner)

Note: AdSize is re-exported by this package. You only need:

import 'package:ads_manager/ads_manager.dart';

No direct import from google_mobile_ads is required just to use AdSize.

Native

A native ad widget that auto-selects provider via Remote Config, with automatic fallback when allowed.

// Default behavior (provider sourced from RC)
const NativeAdManager(height: 300)

// Custom styling
NativeAdManager(
  height: 300,
  style: NativeTemplateStyle(templateType: TemplateType.medium),
)

Events

Listen to the stream for ad lifecycle events to drive UI state (e.g., loading spinners):

final sub = AdsManager.instance.onEvent.listen((event) {
  final type = event.keys.first;  // AdUnitType
  final ev = event.values.first;  // AdEventType
  // handle
});

AdUnitType: banner, interstitial, rewarded, rewardedInterstitial, native, appOpen

AdEventType: loading, loaded, failedToLoad, showed, dismissed, failedToShow, earnedReward

Testing & Debugging

AppLovin MAX Test Mode

The ads manager automatically enables AppLovin MAX mediation debugger during initialization for testing purposes. This provides:

  • Real-time mediation waterfall information
  • Network adapter status
  • Test ad controls
  • Revenue and performance metrics

AdMob Test Ads Helper

You can switch to AdMob test IDs at runtime and preload again:

await AdsManager.instance.showTestAds();

This is handy to validate your integration without touching your production IDs.

Provider-Based Testing

Use the helper to switch to AdMob test IDs. To simulate networks, change RC provider keys at runtime and call AnalyticsManager().refreshRemoteConfig() before showing again.

// Switch to AdMob test IDs and preload test ads
await AdsManager.instance.showTestAds();

// Change RC: banner_provider / interstitial_provider / reward_provider / app_open_provider
await AnalyticsManager().refreshRemoteConfig();

Adjust Subscription Tracking

The ads manager includes a helper to track subscription purchases in Adjust using product metadata. This keeps Adjust usage inside the ads manager package.

void trackSubscriptionWithAdsManager({
  required String eventToken,
  required ProductDetails product,
}) {
  AdsManager.instance.trackSubscriptionPurchase(
    eventToken: eventToken,
    price: product.rawPrice,
    currencyCode: product.currencyCode,
    productId: product.id,
    title: product.title,
    description: product.description,
  );
};

purchase listener (after confirming a successful purchase):

if (purchaseDetails.status == PurchaseStatus.purchased ||
    purchaseDetails.status == PurchaseStatus.restored) {
  final product = getProduct(purchaseDetails.productID);
  if (product != null) {
    trackSubscriptionWithAdsManager(
      eventToken: 'YOUR_ADJUST_SUBSCRIPTION_EVENT_TOKEN',
      product: product,
    );
  }
}

Example

See the example/ app for a full usage demo with buttons for interstitial, rewarded, app open, banner, and native ads plus event-handling.

Notes & Tips

General

  • Always use Google's test IDs or your own test placements during development to avoid policy violations.
  • AppLovin MAX requires your real SDK key and ad unit IDs. Ensure network permissions and platform setup are complete.
  • If you prefer not to use MAX, initialize without max: and the manager will work with AdMob only.

Provider Behavior

  • Provider 0 (no ads): All show methods return false. Banner/Native widgets render nothing and do not load.
  • Provider 1 (AdMob only): No fallback to MAX, even if MAX is configured. Use this for pure AdMob testing.
  • Provider 2 (MAX only): Direct MAX ad loading, bypasses AdMob entirely.
  • Default (null provider): Smart fallback system - tries AdMob first, falls back to MAX on failure.
  • Widgets: Banner and Native widgets respect provider settings and handle fallbacks accordingly.

Performance

  • MAX mediation debugger adds overhead - disable in production builds if needed.
  • Provider selection allows you to test individual networks without interference.
  • Event streaming helps optimize UI responsiveness during ad loading.

License

MIT