Wiring AdMob in Flutter: Eight Hashes for Two Apps

In 2026, integrating AdMob into your Flutter applications has become a more streamlined yet nuanced process. Ongoing updates in both the Flutter ecosystem and AdMob itself, make it crritical that developers ensure test ads are served correctly and reliably on test devices. This article outlines our experience implementing AdMob banners in two Flutter applications, porjects parchment and linen, while addressing the challenges we faced along the way and how to effectively resolve them.

Our two apps are still in production: parchment (a community app) and linen (a citizen-reporting tool). Both are built with Flutter, released via TestFlight and Google Play, and utilise Firebase along with Fastlane + Gitea CI for continuous integration. Recently, we integrated google_mobile_ads banners into both applications. Our testing devices include physical Google Pixel phones, and physical iPhones, test builds are delivered via Google Play Internal Test track and TestFlight respectively. With two apps and several devices, we faced the challenge of marking each device-app pairing as a test device for AdMob to avoid serving real ads during our internal testing, which could lead to violations of AdMob's policies regarding invalid traffic.

TL:DR – Managing test devices for AdMob is much harder than it should be in 2026. It is far too easy to serve live adverts to test devices and risk a policy violation.

What AdMob's documentation suggests

According to Google's updated AdMob Flutter quickstart guide, developers are encouraged to register their test devices in the AdMob console under Settings → Test devices or pass testDeviceIds to MobileAds.instance.updateRequestConfiguration(). The console method uses the device's Identifier for Vendor (IDFV) on iOS or Advertising ID (GAID) on Android. Alternatively, the in-code method uses a 32-character hexadecimal hash that the SDK generates on the first ad request from an unrecognised device.

While these two options appear straightforward, the reality is more complex: developers often find themselves relying on both methods, with failures that can mimic various unrelated issues.

The four identifier systems

Understanding why our AdMob console registrations weren't effective required a deep dive into the different device identifiers used in mobile advertising:

  • IDFV — Apple's Identifier for Vendor, a UUID that remains consistent as long as at least one app from the same publisher is installed on the device. This identifier is crucial for AdMob test-device matching on iOS.
  • IDFA — Identifier for Advertisers, a UUID requiring user consent under App Tracking Transparency (ATT). Without consent, the SDK will revert to IDFV.
  • GAID — Google Advertising ID on Android, similar to IDFA and used for matching in the AdMob console.
  • App-set ID — A newer identifier for Android, which does not require ads-personalisation consent.
  • The SDK hash — A 32-character hex string generated by the AdMob SDK that is specific to the app installation and device combination, solely used by the in-code testDeviceIds method.

The AdMob console accepts the first four identifiers, while the SDK's testDeviceIds list only accepts the fifth.

Consequently, you cannot interchange identifiers between the console and SDK.

Android 16 challenges with console-side matching

Our first device, a Google Pixel phone running Android 16, had its GAID registered in the AdMob console, but we encountered issues when running a release build via Play Internal Testing, as live ads were still served. This occurred because Android 16 has made it harder for users to locate the Advertising ID settings. If a user opts out of personalised ads, the GAID returned to apps will be all zeros (00000000-0000-0000-0000-000000000000), making it virtually impossible for the AdMob console to match devices correctly. To work around this, we used the App-set ID. Our alternative was to obtain the SDK's internal hash through adb logcat | grep RequestConfiguration and incorporate it into our in-code testDeviceIds list. This led to additional complications.

The SDK hash can change frequently

After capturing the Pixel's hash, we added it to our code and confirmed it was in place, yet live ads were still served. Upon further inspection, we discovered that the hash is install-specific on Android—uninstalling and reinstalling the app results in a new hash. This means that for the six device-app pairings we maintain, we need to manage six different hashes, each of which can change under various circumstances.

The Flutter plugin bug that surfaced

Even with the correct hash in our in-code list, live ads continued to be served. We added diagnostic logging to our bootstrap:

final applied = await MobileAds.instance.getRequestConfiguration();
print('[bootstrap] testDeviceIds requested=${AdBanner.testDeviceIds} applied=${applied.testDeviceIds}');

Despite the API confirming that our list was correctly set, the SDK still served live ads. We discovered it was due to a bug in google_mobile_ads 6.x, which had been in place since the apps original release, where the updateRequestConfiguration call was not correctly propagated to the native SDK's request builder. The solution was a straightforward upgrade to google_mobile_ads 8.x, which resolved the issue immediately.

This upgrade is crucial as it requires the Google-Mobile-Ads-SDK version ~> 13.2 (instead of 12.2), necessitating an update to the iOS deployment target to 14.0. This change involves updating the Podfile, running pod update Google-Mobile-Ads-SDK, and managing any other dependencies that might be affected.

If your Flutter project is still using google_mobile_ads 6.x or 7.x and you're experiencing issues with test devices, upgrading to 8.x is essential. Otherwise, you'll have to rely on the AdMob console for device registrations.

The iOS side has its own complexities

On iOS, the test-device registration via the AdMob console appears to be more reliable than on Android, but challenges still exist. After registering the IDFV for an iPhone, we confirmed it appeared in the AdMob console, yet live ads were still being served. To diagnose the issue, we had to rule out several possibilities:

  1. Server-side propagation delays for IDFV registration.
  2. The SDK not being initialised correctly before the initial BannerAd.load() call (which it wasn't).
  3. The ad unit we created may have had insufficient inventory (true, but only part of the story).
  4. The plugin bug previously mentioned.

To rule out the inventory issue, we pointed our iOS banner unit constant at Google's official test unit ID (ca-app-pub-3940256099942544/2934735716) and shipped a TestFlight build, which successfully served test ads. This confirmed that the SDK was functional, and the real unit simply needed more traffic to begin serving ads.

The method for hash extraction on iOS differs from Android. The SDK does not print the "use RequestConfiguration..." hint in a readily accessible manner. Instead, it emits this information through Apple's unified logging system, which requires deeper exploration. We introduced a compile-time toggle in our AdBanner widget to facilitate hash capture during testing.

The bootstrap timing race

Even after applying the correct hash and updating the plugin, the first impression upon launching the app continued to serve live ads, while subsequent impressions served test ads. This problem stemmed from our bootstrap firing MobileAds.instance.initialize() and the subsequent updateRequestConfiguration call as asynchronous operations. The ad requests were being made before the configuration was fully established.

The solution is simple: ensure both calls are awaited:

await MobileAds.instance.initialize();
await MobileAds.instance.updateRequestConfiguration(
  RequestConfiguration(testDeviceIds: AdBanner.testDeviceIds),
);

This adjustment incurs a slight delay in startup time but is essential for ensuring test ads are served correctly.

The iOS Info.plist requirements

On the iOS side, there are two critical elements that must be included in the Info.plist:

  • GADApplicationIdentifier — The app ID from the AdMob console. Omitting this results in SDK crashes upon startup.
  • SKAdNetworkItems — A list of attribution partner IDs that Google publishes for AdMob. This list is vital for ensuring that demand partners can bid effectively and fill rates remain healthy. It's essential to periodically refresh this list to include new partners.

The Android side also has its own requirements

On Android, it’s necessary to include the com.google.android.gms.ads.APPLICATION_ID meta-data tag in AndroidManifest.xml. This serves the same purpose as the iOS GADApplicationIdentifier. If omitted, it can lead to crashes during app startup.

For testing purposes, Google provides sample IDs (ca-app-pub-3940256099942544/...) that can be safely used in development and testing environments. However, these should not be used in production, as failure to replace them could lead to compliance issues.

The bookkeeping headache

Currently, our in-code testDeviceIds lists for both apps look like this (not real Id's obvs):

// linen
static const testDeviceIds = [

'5583FADA2EA14C1F9514A6B2BAB5F2C3', //  Pixel 1
'2A7968F177D942DA9BD535587FADC055', //  Pixel 2
'F9870DC1E549450B89702CC80E0F3079', //  iPhone 1
'CD707B6FE0E14A8B8BF1093EF37EDBD5' //  iPhone 2
];

// parchment
static const testDeviceIds = [
  
  '564D4613C2B04C829C18D117B991672C', //  Pixel 1
  'F0C8EC3903104B258E3D45C645C0447D', //  Pixel 2 
  '827EF0F7FDC44B6D98C57FF776A0DB0D', //  iPhone 1
  '00E6D7F04D604D0FBEFF6219B55D5677' //  iPhone 2
];

Managing these hashes is critical, as they can change due to various factors such as app uninstallation or user actions. The maintenance burden is manageable for four devices and two apps, but scaling up to ten devices across five apps would quickly become difficult.

It’s also important to maintain records in the AdMob console, as some demand partners rely on server-side test flags. This dual approach ensures compliance and enhances the reliability of ad serving.

The plugin version matrix

For Flutter teams setting this up in 2026, here’s the compatibility matrix we established:

ComponentMinimumWhy
google_mobile_ads 8.0.0 6.x versions do not handle testDeviceIds correctly; 7.x is untested; 8.x works as expected.
Google-Mobile-Ads-SDK (iOS pod) 13.2.0 Required by google_mobile_ads 8.x.
Podfile platform :ios 14.0 Required by Google-Mobile-Ads-SDK 13.x.
Android compileSdk 34 Required by recent google_mobile_ads native dependencies.

If you’re working on a recent Flutter project, the Android configuration is likely fine. However, the iOS side may require updates to the Podfile and a refreshed Podfile.lock to ensure all dependencies resolve correctly.

What we'd tell past us

If we could revisit the day we declided to modernise our google_mobile_ads, we would emphasise the following:

  1. Begin with google_mobile_ads 8.x or newer to avoid wasting time with earlier versions.
  2. Establish platform :ios, '14.0' in the Podfile from the outset and commit the Podfile.lock immediately.
  3. Incorporate test-device registration into the app's bootstrap from day one, ensuring both initialize() and updateRequestConfiguration() are awaited before runApp.
  4. Consider the in-code testDeviceIds as the authoritative list, while also maintaining records in the AdMob console for comprehensive coverage.
  5. Create a _forceProdAdUnits compile-time toggle along with structured diagnostics within your AdBanner to capture device hashes and debug SDK issues effectively.
  6. Include the complete Google SKAdNetwork list in the iOS Info.plist from the beginning and refresh it annually.
  7. Understand that new ad units may no-fill for 24–72 hours, regardless of test-device flags. Use Google's official test unit ID during this period to facilitate testing.
  8. Manage each device-app pairing as a distinct test-device record. Multiple devices and apps increase the complexity of maintenance.

Stringent privacy requires developer effort

In 2026, the integration of advertising SDKs like AdMob has shifted to accommodate more stringent privacy regulations and evolving user expectations. Google has made significant efforts to enhance transparency and control for users regarding ad personalisation. Updates to the App Tracking Transparency framework change how identifiers are managed across platforms. Developers must stay aware of these changes, as they can significantly impact ad performance and compliance as well as needing to be disclosed on store listings.

Moreover, recent improvements in Flutter's ecosystem and the google_mobile_ads plugin have streamlined many processes, reducing the complexity of setting up test devices and improving the debugging experience. As a result, developers are encouraged or perhaps forced to adopt the latest versions to benefit from these enhancements.

The sane-world version

Ideally, the AdMob test-device integration would function in a way that prioritises developer experience. This could involve:

  • Utilising a single identifier system that automatically detects both IDFV on iOS and App-set ID on Android, eliminating the need for the SDK hash.
  • Establishing a server-side endpoint for the AdMob console to write its test-device list, which the SDK could query upon initialisation, thus removing the need for an in-code list.
  • Introducing a synchronous setTestDevices() function that could be called before initialisation to eliminate bootstrap timing races.
  • Providing consistent logging across both iOS and Android to capture device identifiers easily.
  • Bundling the SKAdNetwork list directly into the SDK, negating the need for manual upkeep in the Info.plist.

This approach would not require new capabilities but would necessitate a shift in focus towards enhancing the developer experience as a primary feature.

Addendum: Console.app sometimes just refuses

We encountered an issue when trying to register a new testing iPhone: Console.app failed to display the expected logs from AppDelegate.application(_:didFinishLaunchingWithOptions:) during release/TestFlight builds. Despite filtering for the correct log prefix and enabling all logging options, app-level logs were not visible.

The workaround involved displaying the IDFV within the app's UI during the test phase. This temporary widget allowed us to capture the IDFV without relying on Console.app, streamlining the debugging process. The implementation was relatively straightforward and took just one TestFlight cycle to deploy. We would recommend doing this at the outset!

Addendum: IDFV can reset

Another surprising twist was that the IDFV for one iPhone changed unexpectedly after a clean install. According to Apple’s documentation, the IDFV is stable as long as at least one app from the vendor remains installed. However, this incident suggests that there might be edge cases where the IDFV can reset, necessitating a re-registration in the AdMob console after significant changes like app uninstalls.

It's critical to add this to your maintenance checklist: after a clean reinstall or factory reset of any test device, always re-capture and re-register the IDFV to prevent issues with live ads being served.

Coda

As of May 2026, both parchment and linen reliably serve test ads across all devices, thanks to the updates in the Flutter plugin and our troubleshooting efforts. The journey took approximately a week of intermittent work across both apps, which, while lengthy, has provided a robust diagnostic toolkit for future projects. This toolkit includes structured logging and proper configuration protocols that will streamline future ad integrations.

If you find yourself wrestling with issues related to test ads and identifier management, ensure you’re using the latest version of the google_mobile_ads plugin, as many common pitfalls stem from outdated software. Staying current is essential in navigating the intricacies of ad integration in today’s development landscape.