NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
AdMob integration for Flutter with policy-aware defaults: banner, interstitial, rewarded, rewarded interstitial, native and app open ads with GDPR/UMP consent.
Last release 2 months ago
07 Aug 2026
Release timing varies
gaps range from 1 weeks to 5 months
Nearly every release is documented
notes for 34 of 36 stable releases
Nothing withdrawn
no release was ever pulled
9 months old
36 releases · first in 2025
One column per month.
No breaking changes, no migration — every existing call site compiles unchanged. Two behaviours change on purpose (see BEHAVIOUR CHANGES).
A banner sizing release. Fixes issue #15: an anchored adaptive banner reserving far more vertical space than the ad it showed, with the app's own background visible around a narrow, short creative.
No breaking changes, no migration — every existing call site compiles unchanged. Two behaviours change on purpose (see BEHAVIOUR CHANGES).
An anchored adaptive banner is now sized from what the SDK actually
rendered, not from what Dart asked for. google_mobile_ads 9.0.0 cannot
round-trip the "large" bit of an anchored adaptive size: the height query
honours it, but the codec that sends the size back down to the platform
writes only (orientation, width) and both native decoders rebuild it with
isLarge = false. So Dart held the large height while the ad view the
creative rendered in was built at the classic size — on a 426x952dp phone,
a 67dp ad inside a 133dp box. The ~66dp remainder is unpainted by both the
plugin and the SDK, so the app's own surface showed through it, on every
anchored banner, whatever creative served. The seam now resolves the handle's
dimensions via getPlatformAdSize() for both adaptive kinds. Failure
handling is deliberately asymmetric: inline adaptive has no requested height
to fall back on and keeps its existing semantics, while anchored adaptive
falls back to the requested size and never fails or disposes a loadable
ad over a lost size query. This also roughly halves the pre-load layout shift.
Test mode picked the wrong sample ad unit for adaptive banners on
Android. Google publishes a different demo unit per banner format, and
TestAdUnitIds.banner.android was the fixed-size one — so an adaptive
request was answered with fixed IAB creatives (320x50, 320x100, 468x60) that
cannot fill an adaptive slot. The iOS entry was already the adaptive unit, so
the pair was inconsistent and the defect was Android-only. Sample units are
now per-format and selected by BannerKind, per placement.
A banner slot with no usable width no longer requests an ad. The load was gated on the layout width being finite, which admits zero. A zero-width placement (a collapsed panel, a mid-animation container) still resolves to a valid adaptive ad size natively, so nothing refused it: a real, billable ad loaded and rendered in a zero-width box — an impression nobody can see. Now gated on a positive width; a slot that later gains one loads then, and a collapse/expand cycle does not re-request.
AdFlowBanner.backgroundColor — an opaque colour painted behind the
slot (and behind the pre-load placeholder). An adaptive slot is anchored to
its width, so a smaller creative is centred by the SDK with the surround left
unpainted; Google's guidance is an opaque ad-view background. Paints strictly
under the ad, never over it, and disappears entirely while ads are disabled.
Note:
TestAdUnitIds.banneris now an alias ofadaptiveBanner, so its Android value changed (…/6300978111→…/9214589741). Nothing stops compiling. But if you deliberately passedTestAdUnitIds.bannerto aBannerKind.fixedslot — the only sample constant available before 5.3.0 — switch that placement toTestAdUnitIds.fixedBanner. Slots that lettestModeresolve the id are selected by kind automatically.
TestAdUnitIds.adaptiveBanner, TestAdUnitIds.fixedBanner,
TestAdUnitIds.forBannerKind(kind) — Google's documented per-format sample
banner units. TestAdUnitIds.banner is kept as an alias of adaptiveBanner
(the default kind's unit), so existing references still compile.
BannerConfig.defaultKind and an optional kind: on
AdFlowConfig.bannerAdUnitId.
testMode on Android now serves correctly-sized adaptive test creatives.Because the plugin drops the "large" flag, Google's large anchored adaptive format cannot currently be requested from Flutter at all — every anchored request degrades to the classic size. ad_flow is now honest about that (the box matches reality), but it cannot recover the format. There is deliberately no standard-vs-large option, because both Dart factories produce an identical wire message and such a knob would be inert. See ADR-073.
A focused reliability patch completing the 5.2.1 cached-consent fast path. Upgrade: ad_flow: ^5.2.2 .
A focused reliability patch completing the 5.2.1 cached-consent fast path. Upgrade: ad_flow: ^5.2.2.
No public API or default configuration changes; internal cached-consent reconciliation and reactive state accuracy were corrected.
false (consent lapsed and a now-required form was declined). The downgrade invalidates inventory through the existing consent generation before rechecking — so a stale ad whose SDK load was still in flight, and a full-screen show already waiting at an async pre-show check, are now rejected rather than served: the stale handle is disposed, never published as AdLoaded, no impression/show occurs, and the slot settles into an honest blocked state (no duplicate request or retry storm, balanced coordinator).AdFlow.canRequestAds now reflects the accepted cached true result immediately while the fast path is actively serving, then reconciles to the final result. whenReady semantics are unchanged.Cached-false blocking, ATT exclusion, forwardConsent fail-closed, and request-configuration ordering are all unchanged; a final true settlement does not invalidate good inventory or duplicate loads.
No public API, dependency, configuration, or migration change. SemVer patch.
A focused reliability patch that completes the 5.2.1 cached-consent fast path. No public API changes, no default changes, no migration — the change is internal, and every existing call site compiles unchanged.
recheckGate() does nothing while a controller is
AdLoading/AdShowing and — for a non-forwarding app — nothing bumped the
consent generation. The downgrade now invalidates the consent generation
before re-checking, so the completing load's own generation check and the
final show-dispatch guard both reject the stale handle: it is disposed, never
published as AdLoaded, no impression/show occurs, and the slot settles into
an honest blocked state — with no duplicate request or retry storm, and
balanced coordinator state. Cached-false blocking, ATT exclusion,
forwardConsent fail-closed, and request-config-before-load are all
unchanged.AdFlow.canRequestAds reflects the accepted fast path. The documented
live reactive consent value now publishes true the moment the cached-consent
fast path is accepted (it previously stayed false until the slow flow
finished, despite ads already serving), and the full flow later reconciles it
to its real final value (false on a downgrade). whenReady semantics are
unchanged.A focused reliability patch . Upgrade: ad_flow: ^5.2.1 .
A focused reliability patch. Upgrade: ad_flow: ^5.2.1.
No public API or default configuration changes; internal correctness, weak-network recovery, and final dispatch safety were improved.
Returning users with valid cached UMP consent from a previous session no longer wait for this launch's (possibly slow) consent-info update before ads can serve. Per Google's UMP guidance, once this launch's update has been dispatched, a valid cached canRequestAds() lets first-frame banners/natives, full-screen preloads and cold App Open readiness serve immediately instead of stalling on slow or intermittent connections. The full flow still runs and publishes its final result; a downgrade drops the early inventory.
Protections remain intact: requestConsentInfoUpdate() runs every launch and no request precedes its dispatch; a cached-false (first-install) user stays blocked until settled; ATT ordering / a required ATT decision is honored (fast path disabled when a client-driven ATT primer is configured); forwardConsent stays fail-closed and forward-before-init (fast path disabled for forwarding adopters); request-configuration-before-load is unchanged; no duplicate consent flows or load storms.
Full-screen ads are re-validated immediately before the SDK handle.show() dispatch: ads still enabled, the loaded ad's consent generation still current, not expired, not disposed. A disableAds() / consent mutation / expiry that lands during an async pre-show wait — previously skipped while the controller was AdShowing — no longer dispatches a disabled, stale, or expired ad. Revoked inventory is dropped (not left warm); stale/expired inventory is discarded and reloaded. Coordinator balance, exactly-once show(), reward semantics and the rewarded-interstitial intro are unchanged.
Corrected App Open trigger-mode docs (README + AppOpenHandle: launchOnly / resumeOnly / launchAndResume, not "warm-start only") and the runtime-SSV helper doc (load-time attach fails the load closed for a configured SSV, not silent best-effort).
No public API, dependency, migration, or default-configuration change. SemVer patch.
A focused reliability patch. No public API changes, no default changes, no migration — both fixes are entirely internal, and every existing call site compiles unchanged.
canRequestAds() true no longer loses ad serving while this
launch's consent-info update is slow. Per Google's UMP guidance, once this
launch's requestConsentInfoUpdate() has been dispatched, a valid cached
consent lets first-frame banners/natives and full-screen preloads serve
immediately, instead of waiting out the update (up to the 30s timeout) on slow
or intermittent connections. The full flow still runs and publishes its final
result; if it downgrades (consent lapsed and a now-required form was declined)
the inventory that loaded is dropped. All ordering guarantees are preserved:
requestConsentInfoUpdate() still runs every launch and no ad requests before
it is dispatched; a first-install user (cached false) stays blocked until
settled; ATT ordering and required ATT decisions are honored (the fast path is
disabled when a client-driven ATT primer is configured); forwardConsent
stays fail-closed and forward-before-init (the fast path is disabled for
forwarding adopters, whose loads are gated on the barrier regardless);
request-configuration-before-load is unchanged; and there are no duplicate
consent flows or load storms.showEngine() now re-verifies the cheap
synchronous facts — ads still enabled, the loaded ad's consent generation
still current, not expired, not disposed — immediately before the irreversible
handle.show(). A disableAds() or consent mutation that landed during a
prior await (the show-permission or frequency-cap check), or an expiry that
crossed during a slow cap-store hydration, is deliberately skipped by
recheckGate() while the controller is AdShowing — so the previously
captured handle could be dispatched despite being revoked or stale. Now a
revoked ad is dropped (not left warm under adsDisabled), and a stale/expired
one is discarded and reloaded, rather than shown. Interstitial, rewarded,
rewarded interstitial and app open are all covered; coordinator balance,
exactly-once show(), reward semantics and the rewarded-interstitial intro are
unchanged.AppOpenHandle dartdoc said app
open was "warm-start only" (5.1+ has launchOnly / resumeOnly /
launchAndResume trigger modes); and the runtime-SSV helper doc implied
load-time SSV attach is silently best-effort, when in fact an unattachable
configured SSV fails the load closed.A focused reliability release. Backward-compatible — no migration .
A focused reliability release. Backward-compatible — no migration.
No ad request is ever sent while the real MobileAds.initialize() future is still running, on every path:
AdBlockReason.requestConfigNotApplied and fail-open applies only after init has genuinely completed (a config attempted and failed for real).forwardConsent-starts-init-later path — for a mediation forwardConsent adopter the SDK's init only starts after forwarding succeeds, i.e. after the first config check already ran. The gate now re-settles request configuration after the forwarding barrier, so a placement can no longer slip through on the forwarding-path init-wait timeout while init is still in flight. Forward-before-init is preserved.AdLoading, never a request. Startup stays fully non-blocking.disableAds() (Remove-Ads) protectionA disableAds() that lands after a load passed the gate but before the SDK returned its handle no longer publishes the late handle as loaded or keeps it warm. Banner, Native, Interstitial, Rewarded, Rewarded Interstitial, and App Open now re-check the current permission synchronously immediately before installing the handle, drop it if ads are disabled (AdBlocked(adsDisabled)), and re-warm on enableAds(). A transient indeterminate read never drops good inventory.
Rewarded / rewarded-interstitial docs now explain, operationally: onReward is a client-side signal, not proof; attaching userId/customData does not prove backend verification; the backend must verify Google's callback signature and key_id, validate user / ad-unit / reward / custom data, and process transaction_id idempotently; the two fulfillment strategies; a no-double-grant warning; and a link to Google's official Flutter SSV guide.
+AdGate.isEnabled — one additive getter (mirrors AdGate.consentGeneration). This is the entire delta; no removals, no signature/enum/default changes; hence a correct minor. No migration.The upstream google_mobile_ads 9.0.0 App Open failed-load leak remains (the plugin omits dispose() on that branch and its callback carries no ad reference, so the seam cannot fix it) — low-severity, process-bounded.
543 tests pass; analyze & format clean; pana 160/160; publish dry-run 0 warnings; Android and iOS simulator example builds pass. No physical-device run was performed — not required for this pure-Dart lifecycle change.
A focused reliability release: two lifecycle fixes that close windows where
an ad request could go out before the SDK was ready, plus operationally-correct
SSV documentation. Backward-compatible — no migration, and every existing
call site compiles unchanged. This is a minor (not a patch) only because it adds
one small public symbol: an additive, internal-facing getter
AdGate.isEnabled (a synchronous mirror of the injected Remove-Ads/alive
predicate, parallel to the existing AdGate.consentGeneration). No types,
methods, enums or defaults were removed or changed.
forwardConsent path. The request-config gate previously
honoured the fail-open RequestConfigFailurePolicy (the auto default for a
non-policy-sensitive config) even while the real MobileAds.initialize() was
still running, so a first-frame banner or native slot on a weak network /
mediation cold-start could dispatch an ad request before the SDK had
initialized. Fail-open now takes effect only after init has genuinely
completed (a config that was attempted and failed for real); while init is
in flight the slot blocks with AdBlockReason.requestConfigNotApplied. For a
forwardConsent adopter — where the SDK's init only starts after forwarding
succeeds, i.e. after the first config check already ran — the gate now
re-settles request configuration after the forwarding barrier, so a load
can no longer slip through on the forwarding-path init-wait timeout while init
is still in flight. Startup stays fully non-blocking (AdFlow.initialize()
returns immediately, the first frame never waits), gate waits stay bounded,
and the ADR-028 init→updateRequestConfiguration ordering (and
forward-before-init) are preserved. When init completes late, request
configuration is applied promptly and blocked slots recover automatically — no
app code, no long cooldown, no duplicate config calls or load storms. If init
never completes, the UI stays usable and slots settle into an honest blocked
state rather than sending requests or staying AdLoading forever.disableAds() in-flight-load race is closed for every format (banner,
native, interstitial, rewarded, rewarded interstitial, app open). A load that
had already passed the gate but not yet received its SDK handle when
disableAds() (Remove-Ads) fired could still publish the late handle as
AdLoaded / keep it warm, because recheckGate() cannot drop an AdLoading
controller. Each controller now re-reads the cheap, current permission
synchronously — in the same turn it would publish AdLoaded — and, if ads
were disabled while the request was in flight, disposes the returned handle,
never installs it, and reports AdBlocked(AdBlockReason.adsDisabled).
enableAds() re-warms automatically. The existing consent-generation
invalidation is untouched, and — because the re-check is a pure synchronous
bool — a transient internalError can never wrongly drop a good loaded ad.ServerSideVerification / OnUserEarnedReward
dartdoc now spell out that onReward is a client-side completion signal (not
cryptographic proof), that attaching userId/customData does not prove your
backend verified anything, and what a valuable-reward backend must do: verify
Google's callback signature and key_id, validate the user / ad unit /
reward / custom data, and process transaction_id idempotently. It documents
the two valid fulfillment strategies (grant-then-reconcile vs. wait-for-verified
callback), warns against double-granting from both client and server, and links
Google's official Flutter SSV guide.google_mobile_ads 9.0.0 App Open failed-load cleanup
asymmetry (a failed AppOpenAd load is not auto-disposed by the plugin, and
its failure callback carries no ad reference the seam could dispose) still
exists, verified against the installed source. It is a low-severity,
process-bounded plugin-side leak that ad_flow cannot safely fix from outside
the seam; the honest note is retained (ADR-048 / RESEARCH.md §3). No fragile
internal-import/reflection workaround was added.A focused, backward-compatible ad-surface layout bug fix prompted by real emulator screenshots.
A focused, backward-compatible ad-surface layout bug fix prompted by real emulator screenshots.
disableAds(); advanced controller mode on AdBlocked(adsDisabled).SafeArea / Card surfaces when ads are disabled (not just the ad inside them).placeholderHeight remains supported, but adsDisabled always overrides it (zero footprint).Verification: dart format/analyze clean, 537 tests green, coverage 87.3%, pana 160/160, publish dry-run 0 warnings. Android runtime was verified on an emulator with Google test ads (enabled / disabled / re-enabled); the iOS simulator build passed (iOS was not interactively runtime-tested).
A focused ad-surface layout bug-fix, prompted by real emulator screenshots. Backward-compatible — no API changes, no migration; existing call sites keep working unchanged.
disableAds(), a mounted
AdFlowBanner / AdFlowNativeAd previously kept reserving its placeholder
height. Both now collapse to a zero footprint immediately. In widget-first
(adFlow:) mode the collapse is synchronous — the widgets listen to
adFlow.adsEnabled, so it happens on the frame disableAds() is called,
without waiting for the asynchronous controller re-check; advanced controller
mode collapses on AdBlocked(AdBlockReason.adsDisabled). adsDisabled
overrides any explicit placeholderHeight. Re-enabling loads and renders
normally again, with no duplicate loads, controller reminting or request
storms.AdFlowBanner no longer reserves a
speculative adaptive estimate (the old "15% of device height, clamped to
50–90dp"). A loaded banner always uses its exact live handle.dimensions. Before
load: fixed reserves its exact configured height; large anchored
adaptive reserves the documented 50dp floor (Google documents large
anchored adaptive banners as 50–150dp) and then grows to the exact resolved
size once loaded; inline adaptive reserves 0 (its real height is
unknown until onAdLoaded). An explicit placeholderHeight is still honoured
for ordinary non-loaded states, and placeholderHeight: 0 opts into fully
collapsed pre-load behaviour — but it is never honoured while ads are disabled.bottomNavigationBar returns SizedBox.shrink() before constructing
SafeArea (so no empty inset bar remains), and the native Card (title,
padding and border) is hidden entirely — demonstrating that parent decorations
must also be conditionally hidden, not just the ad. Corrected the stale App
Open tile subtitle (the example is configured with launchAndResume, so it no
longer claims "never on a cold launch").placeholderHeight: 0 for no pre-load reservation; parent decorations must be
hidden too. Removed the stale 15% / 50–90dp claim. No migration required —
this is a backward-compatible layout bug fix (ADR-070).A focused reliability + App Open UX minor. Additive and backward-compatible — every 5.0 call site compiles unchanged, and the default App Open behavio
A focused reliability + App Open UX minor. Additive and backward-compatible — every 5.0 call site compiles unchanged, and the default App Open behavior is preserved.
AppOpenConfig.triggerMode selects AppOpenTriggerMode:
resumeOnly — the default, exactly the v5.0 behavior (show on a genuine warm return only).launchOnly — show only at cold launch, via showAtLaunchIfReady().launchAndResume — both.showAtLaunchIfReady()Call it from your real loading screen, right before entering main content. It returns immediately and never waits for an ad, the SDK, consent, or the network — it shows only when an eligible ad is already ready, otherwise returns false at once and your app proceeds. It is one-shot per process launch, so a false result never turns into a surprise App Open later. Cold-launch App Open is best-effort, not guaranteed — it appears only if an ad happened to be ready.
launchOnly stops maintaining an ad it can never show again.NativeConfig.maxAdAge (safe default 55 min; null disables); stale native inventory is dropped and reloaded through the normal gate.AdLoaded; concurrent updates generation-serialized).AppStateEventNotifier.startListening() rejection contained; enableAds()/disableAds() and the App Open manager are inert after dispose().FullScreenAdControllerBase.onLoaded() unchanged; resumeOnly default unchanged.See the CHANGELOG and MIGRATION. Live on pub.dev: https://pub.dev/packages/ad_flow/versions/5.1.0
A focused reliability + App Open UX pass. Additive and backward-compatible — no breaking changes; existing call sites keep working, and the App Open default is unchanged.
AppOpenConfig.triggerMode selects
AppOpenTriggerMode.{launchOnly, resumeOnly, launchAndResume}. Default is
resumeOnly — the exact v5 behaviour.AppOpenAdManager.showAtLaunchIfReady()
(reachable as ads.appOpen.showAtLaunchIfReady()), for launchOnly /
launchAndResume. Call it from your real loading screen right before entering
main content. It shows an already-ready eligible ad and never waits for
network, UMP, SDK init, or a load — returns false immediately otherwise. It
is one-shot per process launch (surviving AdFlow reinitialization), so a
false result can never become a surprise App Open once the user is in main
content. Cold launch is not faked from a lifecycle event. All existing
consent / coordinator / cap / expiry / click-return / blocking-view /
Remove-Ads rules stay authoritative.NativeConfig.maxAdAge (nullable; default 55 min, matching the
full-screen formats; null disables). Native ads now expire and safely
reload, so a long-lived screen never renders stale inventory (Google documents
native ads as expiring after ~1 hour).AdFlowConfig.test(appOpenTriggerMode: …) so the example / tests can opt
into the launch path.AdLoaded before an
in-flight override finished re-attaching, so a state listener or an immediate
show() used the previous payload; concurrent updates could also complete out
of order. Now the loaded handle is finalized (the override settled) BEFORE
AdLoaded is published, a re-attach failure fails the load closed, and every
update is generation-serialized so the latest value wins regardless of native
completion order.AppStateEventNotifier.startListening() rejection in the seam
is now contained (was an unhandled zone error on a misconfigured host).enableAds() / disableAds() after AdFlow.dispose() are now inert
no-ops (they threw a "used after disposed" error before) — consistent with
every other post-dispose call.A post-release audit of 4.0.0 (independent adversarial verification, 25
confirmed findings) plus two release-gate corrections to the mediation
consent lifecycle. The major is driven by mediation-privacy correctness:
consent forwarding now runs before MobileAds.initialize() and fails
CLOSED, a new AdBlockReason case, and the removal of the conceptually
invalid deferMediationInit.
AdFlowConfig.deferMediationInit REMOVED (was in 4.0.0). It called the
plugin's disableMediationInitialization, which — verified against
Google's Android/iOS docs and the plugin source — is a session-wide
disable of Google mediation (an A/B-testing tool: "noop once initialize()
or the first ad request is made"), not a defer/resume. It could not
achieve "set the partner flag, then let adapters come up," and disabling
Google mediation is revenue-harming. Use forwardConsent instead — it now
runs before init (below).AdSdk.disableMediationInitialization() REMOVED from the seam interface
(was in 4.0.0). It backed deferMediationInit and has no correct use (see
above). Affects only code that implements or subclasses AdSdk directly (a
custom seam, or a test double not built on the shipped FakeAdSdk) — remove
the override. Apps using the package normally never touch AdSdk.forwardConsent runs BEFORE MobileAds.initialize(). Mediation
adapters initialize during MobileAds.initialize(), and AppLovin/Meta
read their privacy flag at that point (Google: set it "before you
initialize the Google Mobile Ads SDK"). So ad_flow gathers consent, runs
forwardConsent, and only then initializes the GMA SDK. Fail-CLOSED by
default: a failed/timed-out forward means the SDK is not initialized
and loads are BLOCKED (AdBlockReason.consentNotForwarded), retried in the
background; init + serving recover when forwarding succeeds.
unsafeFailOpen initializes/serves anyway. UI is never blocked —
initialize() returns immediately; only whenReady/loads wait.
Non-adopters keep parallel init.AdBlockReason gained consentNotForwarded — exhaustive switches over
AdBlockReason need the new case (or a wildcard). Non-adopters of
forwardConsent never see it.forwardConsent on AdFlow.initialize — the fail-closed,
before-initialize consent-forwarding barrier for mediation networks that do
not read the IAB TCF string themselves (Unity MetaData, AppLovin US-state,
Meta LDU). Its callback is serialized — never invoked concurrently, even
across the internal 15s wait bound (Future.timeout does not cancel its
source), and a newer consent generation's forward never applies its
partner-SDK side effect before an older one's completes. Generation-guarded.AdFlowConfig.mediationConsentPolicy + MediationConsentFailurePolicy
(failClosed default, unsafeFailOpen = explicit unsafe opt-out)._hydrate bounds itself with a
5s timeout but does not cancel the underlying store reads; a store that hung
past the timeout then resumed would overwrite the (by then authoritative)
in-memory caps with stale persisted state — rolling back a fresh impression
and allowing two full-screen ads back to back. A late read now MERGES
(union history, keep the more-recent last-stamp) and never rolls memory
back.setServerSideVerification
called while a load was in flight reported success but the installed ad
carried the previous payload; it now re-applies the override to the ad the
moment it installs. An attach failure on a warm ad now DROPS that ad (and
warms a fresh one with the new override) instead of leaving it showable with
stale verification.maxAdAge during
it was ignored and the ad showed anyway. show() now re-checks live
permission and expiry after the intro, rolling back rather than showing.guardedCallback now
contains an ASYNC callback's later rejection (an onConsentChanged/
onPaidEvent async closure's Future no longer escapes as an unhandled zone
error). The refreshed-banner paid-event subscription, which bypassed the
guard, now routes through it. New safeUnawaited contains a rejecting
handle dispose()/subscription cancel() during teardown.validate() mirrors every constructor assert (release builds strip
asserts): FrequencyCap.maxPerSession/maxPerHour >= 0,
RetryConfig.maxAttempts >= 0 / jitterFactor in [0,1],
InterstitialConfig.minActionsBetween >= 0, NativeConfig exactly-one.Future.timeout
does not cancel its source, so a forwarder that outran the 15s wait could
be invoked again by a retry while the first invocation was still running —
and an older consent operation could apply its partner-SDK side effect
after a newer one. The un-timeout'd source is now tracked: at most one
forwardConsent runs at a time, strictly ordered.AdController.recheckGate() (which
already runs after every consent mutation) via an internal consent-generation
stamp — no new public method, no new integration step.MediationNetworkExtras asserts against an empty class name (a silent
reflection no-op at request time).^3.0.0 → ^5.0.0; the AdBlockReason
cases (requestConfigNotApplied, internalError, consentNotForwarded)
added to the enumeration; a "What's new" section covers the newer surfaces.doc/MEDIATION_SETUP.md documents forwardConsent as the recommended
fail-closed path, mediationConsentPolicy, and a concrete
MediationNetworkExtras example.A post-release adversarial audit of 4.0.0 plus a mediation-consent lifecycle redesign. The major is driven by mediation-privacy correctness : consent
A post-release adversarial audit of 4.0.0 plus a mediation-consent lifecycle redesign. The major is driven by mediation-privacy correctness: consent forwarding now runs before MobileAds.initialize() and fails closed by default, and the conceptually-invalid deferMediationInit is removed.
📦 pub.dev: https://pub.dev/packages/ad_flow/versions/5.0.0
AdFlowConfig.deferMediationInit and AdSdk.disableMediationInitialization(). Both drove the plugin's session-wide disable of Google mediation (an A/B-testing tool), not a defer/resume — conceptually invalid and revenue-harming. AdSdk removal affects only direct seam implementers; FakeAdSdk/GmaAdSdk are updated.forwardConsent runs BEFORE MobileAds.initialize() and fails CLOSED by default. A failed/timed-out forward means the GMA SDK is not initialized and mediation-capable loads block with AdBlockReason.consentNotForwarded (retried in the background); init + serving recover when forwarding succeeds. UI is never blocked — initialize() returns immediately. Opt out only via MediationConsentFailurePolicy.unsafeFailOpen.AdBlockReason gained consentNotForwarded — exhaustive switches need the new case (or a wildcard).AdFlow.initialize(forwardConsent:) — the fail-closed, before-init consent-forwarding barrier for networks that don't read the IAB TCF string themselves. Serialized (never invoked concurrently, even across the 15s wait bound — Future.timeout does not cancel its source) and generation-guarded.AdFlowConfig.mediationConsentPolicy + MediationConsentFailurePolicy (failClosed default, unsafeFailOpen = explicit unsafe opt-out).safeUnawaited).validate() mirrors every constructor assert (release builds strip asserts).recheckGate() via an internal consent-generation stamp (no new public method).MediationNetworkExtras asserts against an empty class name.Static/tests: flutter analyze clean · 494 tests · coverage 85.2% · pana 160/160 · publish dry-run 0 warnings.
Runtime (Google-demand only, AdFlowConfig.test(), no Unity/overrides): Android emulator — all six formats shown; iOS simulator — banner, native, interstitial, rewarded, rewarded-interstitial shown, app-open loaded/ready. Physical-device testing intentionally deferred.
Unity mediation via gma_mediation_unity is temporarily unavailable with 5.0.0: the latest published adapter (1.8.1) requires google_mobile_ads: ^8.0.0 while 5.0.0 requires ^9.0.0. Google's main has an in-progress 1.9.0 for ^9.0.0 but it is unpublished; this release intentionally uses no git dependency, override, or vendored patch. Documented as an optional-integration limitation in doc/MEDIATION_SETUP.md. Unity mediation itself was not runtime-validated.
Most apps compile unchanged — see MIGRATION.md. Delete deferMediationInit if set; add the consentNotForwarded case to exhaustive AdBlockReason switches; if you use forwardConsent, note it now runs before init and fails closed.
Full notes: CHANGELOG.md.
No silent failure: exception containment + callback isolation, per-load watchdog, SSV fail-closed, rewarded-interstitial atomic show reservation, memo
No silent failure: exception containment + callback isolation, per-load
watchdog, SSV fail-closed, rewarded-interstitial atomic show reservation,
memory-authoritative frequency caps, request-config failure policy
(auto/failOpen/failClosed), honest mediation surfaces and docs.
448 tests, pana 160/160. See CHANGELOG.md and MIGRATION.md.
A production-hardening major from an independent adversarial audit of 3.0.0. Theme: no silent failure — collaborator faults, lost SDK callbacks, failed policy-critical configuration and unattachable reward verification now either recover visibly or refuse visibly, never wedge or degrade silently. See MIGRATION.md for the short 3.x → 4.0 checklist.
AdBlockReason gained cases (requestConfigNotApplied,
internalError) and AdFlowErrorKind gained ssv — exhaustive
switches over these enums need the new cases.AdFlowError(ssv), normal retry) instead of a ready ad that
silently lost its verification payload. Honesty note: the plugin acks the
SSV call unconditionally native-side, so only channel-level faults are
detectable — final confirmation is always your SSV endpoint.show(). Behavior change (revises
ADR-039): the rewarded interstitial is no longer exempt from the global
frequency cap — its intro is an app-chosen interruption; a capped
sequence simply never starts. Classic rewarded stays exempt.RequestConfigFailurePolicy {auto, failOpen, failClosed} on
AdFlowConfig (default auto): when updateRequestConfiguration fails
or times out and the config carries policy-critical fields (COPPA /
under-age tags, content rating, test device IDs), loads BLOCK visibly
(AdBlocked(requestConfigNotApplied)) and recover when the retried apply
succeeds — instead of silently sending untagged requests. A config with
no such fields keeps failing open. The apply also never races a live SDK
init (ADR-028 hardening; it previously dispatched right after a timed-out
init — the exact deadlock window).AdGate constructor: configReady (future) replaced by
settleRequestConfig (bounded callback).AdSdk gained disableMediationInitialization();
FakeAdSdk gained knobs (ssvAttachError, dispatch counters,
fullScreenRequests). Package-provided fakes are updated; external
AdSdk implementations must add the new member.RetryConfig.loadTimeout (default 60s, null
disables): the plugin has no load timeout of its own, so a lost SDK
callback used to pin a slot at AdLoading for the whole session. A
timed-out attempt fails into the normal retry path; a LATE completion is
disposed, never installed, and can never stomp a newer attempt.AdRequestOptions on every format config (keywords,
contentUrl, nonPersonalizedAds, AdMob-adapter extras) plus
MediationNetworkExtras mapped onto the plugin's mediation-extras
mechanism.AdFlow.onConsentChanged — fires (isolated) after every consent flow
or mutation: the forwarding point for per-network mediation consent APIs
(Google does not propagate consent to non-TCF networks automatically).AdFlowConfig.deferMediationInit — defers mediation adapter init out
of SDK init so pre-init privacy flags can be set after consent settles.AdBlockReason.internalError,
re-checked on backoff); the whole load body sits in one try; app
callbacks (onPaidEvent, onAdBlocked, reward grants,
onConsentChanged) are isolated via FlutterError.reportError. A
throwing canRequestAds() used to pin slots at AdLoading forever with
an unhandled async error.UmpConsentGateway.ensureCanRequestAds honours its "never throws"
contract on the final canRequestAds() read; Gma handles close their
event streams even when the channel dispose rejects.Production hardening (2026-07 multi-agent audit, 25 confirmed findings fixed) + backward-compat-lifted API cleanup. Breaking: AdBlocked(reason) AdLoad
Production hardening (2026-07 multi-agent audit, 25 confirmed findings
fixed) + backward-compat-lifted API cleanup. Breaking: AdBlocked(reason)
AdLoadState case, widget-first AdFlowBanner/AdFlowNativeAd, show() reward
callback only on rewarded formats, AdGate.canShow removed, showOnColdStart
removed. Added: runtime SSV, mediation observability (AdResponseSummary,
paid-event slot/adSourceName), maxAdAge expiry, live canRequestAds,
config validation, drop-on-Remove-Ads/consent-withdrawal.
Validated on Android emulator + iOS simulator with Google test IDs.
See CHANGELOG.md and MIGRATION.md.
Two releases in one (2.2.0 was never published): the production-hardening work from a deep 2026-07 multi-agent audit (25 confirmed findings, all fixed), plus the API cleanup that backward compatibility had forbidden. See MIGRATION.md for the short 2.x → 3.0 checklist.
AdBlocked(reason) is a new AdLoadState case. A load refused by
policy (consent pending, Remove-Ads, withdrawal, disposed graph) now
reports itself as a state instead of an AdIdle indistinguishable from
"nothing requested yet" — the model ADR-045 documented as correct but
could not ship in 2.x. Exhaustive switches gain one case; the controller
still re-checks its gate and proceeds to AdLoading on its own.AdFlowBanner(adFlow: ads) /
AdFlowNativeAd(adFlow: ads) create AND own their controller, making the
ADR-029 footgun (minting a controller inside build() → permanently
blank ad) unrepresentable. controller: is now optional (advanced use).FullScreenAdController.show() takes no reward callback — it was
silently ignored by interstitial and app-open. The rewarded formats keep
show({onReward}).AdGate is a pure permission gate: the racy composed canShow()
query (review finding #6) and its caps/coordinator collaborators are
removed. Show pacing lives in the controllers, where the atomic
tryEnter() is.AppOpenConfig.showOnColdStart removed (deprecated + ignored since
2.1.0; it never could do anything). Banner/native slot constants renamed
slot → slotName to match the full-screen formats.AdFlow.canRequestAds — a ValueListenable<bool> with the LIVE consent
answer: follows a late consent grant (ADR-035 retry) and a
privacy-options withdrawal, unlike the one-shot whenReady snapshot.BannerAd (a native view), destroy a fresher
right-width ad, corrupt the recorded width, or cancel the slot's only
recovery timer (wedging it blank). resize() now defers to an in-flight
refresh; the refresh completion re-validates state and reconciles a
mid-flight width change; the failure path backs off only while a current ad
exists.AdWidget
cannot re-point its platform view at a new ad, so an unkeyed rebuild after
a swap kept hosting the DISPOSED ad — a permanently dead slot that still
requested (and paid for) fresh ads. AdFlowBanner/AdFlowNativeAd now key
the hosted subtree by handle identity, forcing a correct remount.BannerHandle.dimensions
listenable (inline adaptive creatives vary per refresh).maxAdAge on interstitial/rewarded/rewarded-interstitial configs (default
55 min; null disables) — stale warm ads are proactively replaced and never
shown. App-open's 4h expiry now runs through the same shared mechanism and
also replaces proactively.disableAds() (Remove-Ads),
dispose(), a re-initialize, and a consent withdrawal through
ads.consent now DROP live banner/native ads and warm full-screen
inventory (previously only future loads were blocked — a mounted banner
kept serving and auto-refreshing). enableAds() re-warms at once. New
AdController.recheckGate().show() call for up to 30s. The show path now uses cheap live checks
only (AdGate.showBlockReason).onAdClosed now starts a 3s grace clock;
Android's external-browser return ordering stays suppressed.AdFlowError and no longer leak the constructed ad + stream controllers.SharedPrefsKeyValueStore reads type-corrupt data as absent instead of
throwing (a throwing cap read blocked every full-screen show, with no
self-heal).PrivacyOptionsButton failures default to FlutterError.reportError
instead of a silent swallow.setServerSideVerification(ssv) on both rewarded
controllers — set userId after login and per-show customData; applies
to the warm ad and future loads; throws if attaching fails.AdResponseSummary (handle.response /
controller.response) — winning ad source, adapter class, response ID.
AdPaidEvent gains slot and adSourceName for analytics-ready
impression logging.AdFlowConfig.validate() (run automatically): empty ad-unit strings and
nonsensical durations fail fast at init.interstitialOrNull, rewardedOrNull,
rewardedInterstitialOrNull, appOpenOrNull, appOpenControllerOrNull.FakeBannerHandle.simulateResize/responseSummary,
FakeFullScreenAdHandle.simulateShowFailed/ssvUpdates/ssvUpdateError,
FakeAdSdk.onPrivacyOptionsFormShown.AdFlow.consent now returns a thin graph-aware wrapper: consent-mutating
calls trigger a permission re-check across every controller (this is what
makes withdrawal drop live ads). Read-only members delegate unchanged.doc/MEDIATION_SETUP.md and doc/NATIVE_ADS_SETUP.md rewritten for
v2 (they still described the removed v1 API); README documents the
emergency kill-switch pattern, the Families app-open prohibition, and
both-platform ad unit configuration.BannerHandle gained dimensions; all handles gained response; the
rewarded handles gained updateServerSideVerification; AdController
gained recheckGate(). The in-package fakes implement all of these — custom
implementations must add them.
Docs-only release. The 2.1.0 doc corrections landed on main in 7a47012 but after the v2.1.0 tag, so pub.dev's rendered page still shows the pre-fix RE
Docs-only release. The 2.1.0 doc corrections landed on main in 7a47012 but after
the v2.1.0 tag, so pub.dev's rendered page still shows the pre-fix README. This
bump republishes so the page is current.
No code changes — lib/ is byte-identical to 2.1.0.
dart format clean (0 changed), analyze clean, 313 tests pass.
Co-Authored-By: Claude Opus 4.8 noreply@anthropic.com
Docs: README updated to 2.1.x; documented the diagnostic surface
(AdBlockReason / onAdBlocked / lastBlockReason) and the rewarded
global-cap exemption; fixed a stale skill trap. No code changes.
`AppOpenConfig.showOnColdStart` is deprecated and ignored (ADR-043). It could never do what its name promised, and its only real effect is now the def…
Behaviour and default changes from the eight judgment calls raised by the 2.0.2 audit, all approved by the maintainer. No breaking API changes — every existing call site still compiles. But several DEFAULTS and BEHAVIOURS changed deliberately; read this section before upgrading. See MIGRATION.md for the upgrade checklist and ADR-039 … ADR-045 for the reasoning.
RewardedConfig.cap / RewardedInterstitialConfig.cap (unlimited by
default) if you do want a per-format limit.BannerConfig.minRefresh now defaults to null = no client-side refresh at
all (ADR-041). AdMob already auto-refreshes banner ad units server-side, from
the console, on by default; the client timer was a second, unsynchronised
refresh loop on the same placement — up to 2x the ad requests for no extra
revenue. Set the refresh rate in the AdMob console. Pass minRefresh:
explicitly to opt back in.AppOpenConfig.showOnColdStart is deprecated and ignored (ADR-043). It
could never do what its name promised, and its only real effect is now the
default. Remove it.AdFlow.setBlockingViewAdVisible(bool) lets the app declare that a blocking
banner occupies the screen, so no app-open ad covers it. ad_flow cannot judge
that itself — whether a banner is "blocking" is a question about your layout —
so placement remains partly the integrator's job.AdFlow.initialize() is now idempotent (ADR-044). A second call used to
build a whole new graph and leave the previous one fully alive — still
listening to the foreground stream, still preloading, still able to show ads,
and coordinating through its own separate coordinator, so it could not even see
the new graph's ads. Two app-open reactors, each blind to the other. It now
disposes the previous graph.AdBlockReason + AdFlow.onAdBlocked + controller.lastBlockReason
(ADR-045) — the answer to "why aren't my ads showing?". A refused load reported
plain AdIdle, which is also what "nothing requested yet" looks like, so
consent-not-gathered, Remove-Ads and a frequency cap all looked identical, and
the package logged nothing. Deliberately not a new AdLoadState case:
AdLoadState is sealed, and adding one would break every exhaustive switch
in every app.AdFlow.setBlockingViewAdVisible(bool); BannerAdController.revision,
.resize(), .loadedWidth; StoredFrequencyCapPolicy.globalCapExemptSlots;
AdGate.loadBlockReason(); FullScreenAdCoordinator.noteViewAdOpened() /
.consumeViewAdOpened() / .blockingViewAdVisible.Docs: added a "Set up with AI" README section with copy-paste new-setup and v1→v2 migration prompts. No code changes.
Ground-up rewrite targeting google_mobile_ads ^9.0.0. Breaking — see MIGRATION for the field-by-field and symbol-by-symbol mapping.
Ground-up rewrite targeting google_mobile_ads ^9.0.0. Breaking — see
MIGRATION for the field-by-field and
symbol-by-symbol mapping.
RewardedIntroScreen + injected presenter).recordUserAction +
minActionsBetween), opt-in by first use.onPaidEvent impression-level revenue callback for every format.package:ad_flow/ad_flow_testing.dart ships FakeAdSdk so apps
can unit-test their ad integration.--dart-define=USE_NEXT_GEN_SDK=true (no Dart changes).AdFlow.initialize() — builds the graph synchronously
and returns immediately; consent/ATT/SDK-init run in the background. Render
your first frame at once (no FutureBuilder<AdFlow> spinner). Optional
Future<bool> ads.whenReady awaits the consent gate. Nothing loads before
the gate opens (ADR-032).initializeWithExplainer, now decoupled from BuildContext via presenters
(attExplainer/consentExplainer on initialize, ready-made
AttExplainerScreen/ConsentExplainerScreen). Supplying attExplainer
enables client-driven ATT (iOS). Additive — pass nothing for today's
UMP-driven behaviour (ADR-030).AdSdk seam is the only door to the plugin; state is
ValueListenable<AdLoadState>.ConsentGateway Futures;
ATT handled by UMP (dependency on app_tracking_transparency removed);
consent failures degrade gracefully with a typed lastError.inactive as backgrounding);
foreground detection now uses AppStateEventNotifier; 4-hour expiry
enforced with discard-and-reload.isUsingTestAds false positives — test mode is an explicit
config flag, never derived from resolved IDs.google_mobile_ads re-export are gone — see MIGRATION §7.NEW: EasyBannerAd now supports optional SafeArea wrapping
EasyBannerAd now supports optional SafeArea wrapping (#6)
useSafeArea parameter (default: true) to prevent extra black spacefalse when the banner is already inside a SafeArea or Scaffold that handles insets_wrapWithSafeArea() helper in EasyBannerAd for cleaner SafeArea logicmain (requires PR review before merge)Re-release of v1.3.16 (no code changes)
FIX: App Open ad no longer shows immediately after closing an interstitial or rewarded ad
paused → resumed) from fullscreen ad overlays was mistaken for a real foreground eventAppLifecycleReactorInterstitialAdManager and RewardedAdManager now signal showing/dismiss to the reactorIMPROVED: Comprehensive README rewrite with step-by-step integration guide
ignoreCooldown interstitial exampleAdSdk abstraction and AdManagerMixin for testabilityPrivacyRequirementMixin for consent checksNEW: Non-blocking initialization for instant app startup
waitForInit() method - waits for initialization to complete
Future<bool> indicating if ads can be requestedinitStream - broadcast stream that emits when initialization completes
EasyBannerAd and EasyNativeAd are now fully reactive
initStream on mountwaitForInit() behaviorFIX: Ad managers now properly guard against dispose-during-retry crashes
_isDisposed flag to InterstitialAdManager, RewardedAdManager, AppOpenAdManager, NativeAdManagersetState() called after dispose() errors in edge casesList.of() when notifying listenersConcurrentModificationError if listener removes itself during callbackmeta import in ad_service.dartEasyPrivacySettingsButton and PrivacySettingsListTileFIX: Splash screen remains too long when AdMob initialization is slow
consentNetworkTimeout (default: 10s) - Timeout for consent info network request, falls back to cached statussdkInitTimeout (default: 8s) - Timeout for Mobile Ads SDK initialization, retries in backgroundcoldStartAdTimeout (default: 3s) - Timeout for cold-start app open ad loadingFIX: AppOpenAdManager.addStatusListener callback now fires correctly
AppOpenAdManager.addStatusListener callback now fires correctly (#3)
showAdIfAvailable()_notifyStatusListeners() on show/dismiss/fail eventsskipGdprConsentIfAttDenied config option (default: true)false if you legally require showing GDPR consent regardless of ATTConsentManager.lastAttStatus and isAttDenied getters
NEW: EasyBannerAd now supports custom ad sizes
EasyBannerAd now supports custom ad sizes
EasyBannerAd(adSize: AdSize.mediumRectangle) for fixed-size bannersbanner, largeBanner, mediumRectangle, leaderboard, etc.adSize > collapsible > adaptive (default)FIX: Export BannerAdListener from google_mobile_ads (fixes #1)
BannerAdListener from google_mobile_ads (fixes #1)
BannerAd instances directlyBannerAdManager.loadBanner() method for custom ad sizes
AdSize.mediumRectangle (300x250) for dialogsloadAdaptiveBanner()NEW: Mediation support for third-party ad networks
MediationHelper class for forwarding consent to mediation networksinitialize() / initializeWithExplainer()doc/MEDIATION_SETUP.md for complete integration guideFIX: NativeAdWidget now respects AdsEnabledManager.isDisabled on initial build
NativeAdWidget now respects AdsEnabledManager.isDisabled on initial buildEasyNativeAd and NativeAdWidget ads-disabled behaviorNEW: EasyNativeAd now collapses when ads fail to load (no more empty white space)
EasyNativeAd now collapses when ads fail to load (no more empty white space)
hideOnLoading parameter (default: true) - collapses while loadinghideOnError parameter (default: true) - collapses on load failure (e.g., no fill)false to show loading/error widgets with reserved heightBannerAdManager causing static analysis warningbottomNavigationBarFIX: All ad managers now respect AdsEnabledManager.isDisabled state
AdsEnabledManager.isDisabled state
loadAd() and showAd() check disabled state before proceedingdisableAds() in onComplete was too lateBannerAdManager, InterstitialAdManager, RewardedAdManager, AppOpenAdManager, NativeAdManagerFIX: Applied dart format to all files for pub.dev static analysis compliance
dart format to all files for pub.dev static analysis complianceIMPROVED: Code quality improvements across all ad managers
AdFlowConfigpreloadAds() now only preloads ad types that have real IDs configuredhasBannerConfigured, hasInterstitialConfigured, etc. gettersreset() now properly calls AdFlowConfig.resetCurrent()
example_with_explainer.dart - GDPR-friendly with explainer dialogexample_without_explainer.dart - Direct initializationNEW: Added AdFlow.instance.reset() for testing
AdFlow.instance.reset() for testing
ad_service.dart)use_build_context_synchronously warnings in BannerAdManagerAdFlow singleton testsEasyBannerAd widget testsConsentManager testsad_flow_service.dart fileNEW: Added EasyPrivacySettingsButton widget for GDPR compliance
EasyPrivacySettingsButton widget for GDPR compliance
PrivacySettingsListTile for settings screensinitializeWithExplainer() now properly checks AdsEnabledManager
initialize() behaviorisPrivacyOptionsRequired() now returns correct cached value
canRequestAds instead of privacy options statusAdded explicit platform support declaration for Android and iOS
Banner ads (adaptive and collapsible)
Code formatting fixes for pub.dev static analysis compliance
Nothing published for this version
Nothing published for this version
Your coding agent can read these notes before it upgrades. Set up the MCP server →