Advertisement

Optimizing AdMob App Open Ads and iOS ATT Implementation in Flutter

2026-03-10

Integrating Google AdMob, specifically App Open Ads, into an iOS application requires careful orchestration with Apple's App Tracking Transparency (ATT) framework. In our Video2Gif project, we encountered specific challenges regarding popup conflicts, cold/warm start behaviors, and ad fill rates.

This article documents the technical hurdles we faced and the architectural solutions we implemented to ensure a compliant and reliable ad experience.

1. Core Challenges and Troubleshooting

1.1 The "Vanishing" ATT Prompt

Symptom: Calls to AppTrackingTransparency.requestTrackingAuthorization() would return TrackingStatus.notDetermined immediately without displaying any UI to the user.

Root Cause: On iOS 15+, system-level permission requests (such as "Local Network" or "Notification" permissions) often trigger automatically at launch. If the ATT request is fired concurrently, the system prioritizes its own dialogs and suppresses the ATT prompt.

Solution: We introduced a deterministic delay (2-3 seconds) post-launch before requesting ATT authorization. This buffer allows system dialogs to clear, ensuring the ATT prompt is successfully presented.

1.2 App Open Ads Not Displaying

Symptom: Ads failed to appear during both cold starts (app launch) and warm starts (returning from background).

Root Cause:

  • Cold Start: AdMob initialization depends on ATT consent. If the ATT request is delayed (as per 1.1), the app's home screen often renders before the ad SDK is ready and an ad is loaded.
  • Warm Start: Flutter's main() function only runs on cold start. To show ads when a user resumes the app, we must explicitly listen to the AppLifecycleState.resumed state.

1.3 TestFlight Ad Fill Issues

Symptom: Ads rendered correctly in local Debug/Release builds but failed to load in TestFlight builds.

Root Cause: AdMob treats TestFlight as a production environment. Newly created Ad Units often experience a "learning phase" or propagation delay, resulting in temporary "No Fill" errors despite correct implementation.

2. Technical Implementation Strategy

2.1 Optimal Timing for ATT Requests

Directly invoking permission requests in main() or initState is unreliable.

  • Best Practice: Use WidgetsBinding.instance.addPostFrameCallback combined with Future.delayed.
  • Duration: A 3-second delay proved to be the "sweet spot" for avoiding conflicts with iOS local network permission dialogs.

2.2 Lifecycle Management

App Open Ads are designed for the "resume" scenario. We implemented WidgetsBindingObserver on our primary screen to detect when the app enters the foreground (didChangeAppLifecycleState) and trigger the ad display logic.

2.3 Preloading Strategy

Latency is critical for App Open Ads.

  • Preload: Always load the next ad immediately after the current one is dismissed.
  • Expiration: Check isAdAvailable before showing, as ad objects typically expire after 4 hours.

3. Solution Code

We adopted a "Delayed Request + Lifecycle Listener" pattern.

3.1 Home Screen Logic (CreateScreen.dart)

This component orchestrates the permission request and listens for app resumes.

class _CreateScreenState extends ConsumerState<CreateScreen> with WidgetsBindingObserver {
  @override
  void initState() {
    super.initState();
    // 1. Register lifecycle observer
    WidgetsBinding.instance.addObserver(this);
    
    // 2. Delay ATT request to avoid system dialog conflicts
    WidgetsBinding.instance.addPostFrameCallback((_) async {
      await Future.delayed(const Duration(seconds: 3));
      _checkAndRequestATT();
    });
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    // 3. Handle Warm Start
    if (state == AppLifecycleState.resumed) {
      // Re-check permissions and show ad if available
      _checkAndRequestATT().then((_) {
        AdManager.showAppOpenAdIfAvailable();
      });
    }
  }

  Future<void> _checkAndRequestATT() async {
    final status = await AdManager.initialize();
    // Logic to handle status...
  }
}

3.2 Ad Manager (AdManager.dart)

A singleton-like class to encapsulate initialization and ad display logic.

class AdManager {
  static Future<TrackingStatus> initialize() async {
    // 1. Request ATT first
    final status = await AppTrackingTransparency.requestTrackingAuthorization();
    
    // 2. Initialize AdMob (handles restricted data if consent denied)
    await MobileAds.instance.initialize();
    
    // 3. Preload the first ad
    loadAppOpenAd();
    
    return status;
  }

  static void showAppOpenAdIfAvailable() {
    if (!isAdAvailable) {
      loadAppOpenAd();
      return;
    }
    
    if (_isShowingAppOpenAd) return;

    _appOpenAd!.fullScreenContentCallback = FullScreenContentCallback(
      onAdDismissedFullScreenContent: (ad) {
        ad.dispose();
        _appOpenAd = null;
        loadAppOpenAd(); // Immediate preload for next time
      },
    );
    _appOpenAd!.show();
  }
}

4. Pre-Release Checklist

Before submitting to the App Store, verify:

  • Info.plist contains NSUserTrackingUsageDescription.
  • Info.plist contains GADApplicationIdentifier.
  • ATT dialog appears after the 3-second delay on fresh install.
  • App Open Ad displays when resuming the app from the background.
  • Correct Ad Unit IDs are used (Test IDs for Debug, Production IDs for Release).
Advertisement