ad_flow
AdMob integration for Flutter with policy-aware defaults: banner, interstitial, rewarded, rewarded interstitial, native and app open ads with GDPR/UMP consent.
5.3.0
faizahmaddae/ad_flow
What this package is like to depend on
Last release 16 days 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
8 months old
36 releases · first in 2025
36 releases in the last 12 months
see the full history below
Release timeline
36 releases · Dec 2025 to Aug 2026Releases
latest 36-
5.3.007 Aug 2026Release notes
Open source →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).
FIXED
-
An anchored adaptive banner is now sized from what the SDK actually rendered, not from what Dart asked for.
google_mobile_ads9.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 withisLarge = 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 viagetPlatformAdSize()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.androidwas 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 byBannerKind, 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.
ADDED
-
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.banneris kept as an alias ofadaptiveBanner(the default kind's unit), so existing references still compile. -
BannerConfig.defaultKindand an optionalkind:onAdFlowConfig.bannerAdUnitId.
BEHAVIOUR CHANGES
- An anchored adaptive banner's box now matches the rendered ad — typically about half its previous height on a phone. Nothing about the ad request, fill or revenue changes; the box was simply too tall before.
testModeon Android now serves correctly-sized adaptive test creatives.
KNOWN UPSTREAM LIMITATION
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.
-
-
5.2.221 Jul 2026Release notes
Open source →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.
What changed
- 5.2.1 introduced safe cached-consent startup serving: an eligible returning user with valid cached UMP consent serves ads while this launch's consent-info update is still in flight.
- 5.2.2 completes downgrade reconciliation when this launch's final consent result becomes
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 asAdLoaded, no impression/show occurs, and the slot settles into an honest blocked state (no duplicate request or retry storm, balanced coordinator). AdFlow.canRequestAdsnow reflects the accepted cachedtrueresult immediately while the fast path is actively serving, then reconciles to the final result.whenReadysemantics are unchanged.
Preserved
Cached-
falseblocking, ATT exclusion,forwardConsentfail-closed, and request-configuration ordering are all unchanged; a finaltruesettlement does not invalidate good inventory or duplicate loads.Compatibility
No public API, dependency, configuration, or migration change. SemVer patch.
Release notes
Open source →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.
FIXED
- Cached-consent downgrade now invalidates inventory in flight. When the
fast path served a returning user on cached consent and this launch's flow
then concluded ads may not be requested (consent lapsed and a now-required
form was declined), 5.2.1 dropped an already-loaded ad but could still
install an ad whose SDK load was in flight at the moment of the downgrade
(and could let a full-screen show already waiting at an async pre-show check
dispatch), because
recheckGate()does nothing while a controller isAdLoading/AdShowingand — 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 asAdLoaded, 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,forwardConsentfail-closed, and request-config-before-load are all unchanged. AdFlow.canRequestAdsreflects the accepted fast path. The documented live reactive consent value now publishestruethe moment the cached-consent fast path is accepted (it previously stayedfalseuntil the slow flow finished, despite ads already serving), and the full flow later reconciles it to its real final value (falseon a downgrade).whenReadysemantics are unchanged.
-
5.2.121 Jul 2026Release notes
Open source →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.
Cached-consent startup recovery
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);forwardConsentstays 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.Final full-screen dispatch guard
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. AdisableAds()/ consent mutation / expiry that lands during an async pre-show wait — previously skipped while the controller wasAdShowing— 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-onceshow(), reward semantics and the rewarded-interstitial intro are unchanged.Documentation
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).Compatibility
No public API, dependency, migration, or default-configuration change. SemVer patch.
Release notes
Open source →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.
FIXED
- Cached-consent startup fast path. A returning user whose previous session
already made
canRequestAds()true no longer loses ad serving while this launch's consent-info update is slow. Per Google's UMP guidance, once this launch'srequestConsentInfoUpdate()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 (cachedfalse) 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);forwardConsentstays 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. - Final full-screen dispatch guard.
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 irreversiblehandle.show(). AdisableAds()or consent mutation that landed during a priorawait(the show-permission or frequency-cap check), or an expiry that crossed during a slow cap-store hydration, is deliberately skipped byrecheckGate()while the controller isAdShowing— so the previously captured handle could be dispatched despite being revoked or stale. Now a revoked ad is dropped (not left warm underadsDisabled), 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-onceshow(), reward semantics and the rewarded-interstitial intro are unchanged.
DOCS
- Corrected three stale claims: the README and
AppOpenHandledartdoc said app open was "warm-start only" (5.1+ haslaunchOnly/resumeOnly/launchAndResumetrigger 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.
- Cached-consent startup fast path. A returning user whose previous session
already made
-
5.2.021 Jul 2026Release notes
Open source →A focused reliability release. Backward-compatible — no migration.
Never load while Mobile Ads init is in flight
No ad request is ever sent while the real
MobileAds.initialize()future is still running, on every path:- Ordinary first-frame fail-open / vacuous config — the request-config gate no longer honours fail-open while init is still running; it blocks with
AdBlockReason.requestConfigNotAppliedand fail-open applies only after init has genuinely completed (a config attempted and failed for real). - The
forwardConsent-starts-init-later path — for a mediationforwardConsentadopter 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. - Prompt recovery — when init completes late, request configuration is applied at once and blocked slots recover automatically (no app code, no long cooldown, no duplicate config calls or load storms). A never-completing init leaves slots in an honest blocked state, never
AdLoading, never a request. Startup stays fully non-blocking.
In-flight
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 onenableAds(). A transient indeterminate read never drops good inventory.SSV backend-security guidance
Rewarded / rewarded-interstitial docs now explain, operationally:
onRewardis a client-side signal, not proof; attachinguserId/customDatadoes not prove backend verification; the backend must verify Google's callback signature andkey_id, validate user / ad-unit / reward / custom data, and processtransaction_ididempotently; the two fulfillment strategies; a no-double-grant warning; and a link to Google's official Flutter SSV guide.Public API
+AdGate.isEnabled— one additive getter (mirrorsAdGate.consentGeneration). This is the entire delta; no removals, no signature/enum/default changes; hence a correct minor. No migration.
Known limitation
The upstream
google_mobile_ads9.0.0 App Open failed-load leak remains (the plugin omitsdispose()on that branch and its callback carries no ad reference, so the seam cannot fix it) — low-severity, process-bounded.Verification
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.
Release notes
Open source →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 existingAdGate.consentGeneration). No types, methods, enums or defaults were removed or changed.FIXED
- No ad request is ever sent while Mobile Ads initialization is still in
flight — not even for a fail-open / vacuous request configuration, and not
on the mediation
forwardConsentpath. The request-config gate previously honoured the fail-openRequestConfigFailurePolicy(theautodefault for a non-policy-sensitive config) even while the realMobileAds.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 withAdBlockReason.requestConfigNotApplied. For aforwardConsentadopter — 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→updateRequestConfigurationordering (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 stayingAdLoadingforever. - The
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 whendisableAds()(Remove-Ads) fired could still publish the late handle asAdLoaded/ keep it warm, becauserecheckGate()cannot drop anAdLoadingcontroller. Each controller now re-reads the cheap, current permission synchronously — in the same turn it would publishAdLoaded— and, if ads were disabled while the request was in flight, disposes the returned handle, never installs it, and reportsAdBlocked(AdBlockReason.adsDisabled).enableAds()re-warms automatically. The existing consent-generation invalidation is untouched, and — because the re-check is a pure synchronous bool — a transientinternalErrorcan never wrongly drop a good loaded ad.
DOCUMENTATION
- Server-Side Verification (SSV) guidance is now operationally correct. The
README rewarded section and the
ServerSideVerification/OnUserEarnedRewarddartdoc now spell out thatonRewardis a client-side completion signal (not cryptographic proof), that attachinguserId/customDatadoes not prove your backend verified anything, and what a valuable-reward backend must do: verify Google's callbacksignatureandkey_id, validate the user / ad unit / reward / custom data, and processtransaction_ididempotently. 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.
KNOWN LIMITATIONS
- The upstream
google_mobile_ads9.0.0 App Open failed-load cleanup asymmetry (a failedAppOpenAdload 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.
- Ordinary first-frame fail-open / vacuous config — the request-config gate no longer honours fail-open while init is still running; it blocks with
-
5.1.121 Jul 2026Release notes
Open source →A focused, backward-compatible ad-surface layout bug fix prompted by real emulator screenshots.
- Remove-Ads now immediately collapses Banner and Native surfaces to zero. Widget-first mode collapses synchronously on
disableAds(); advanced controller mode onAdBlocked(adsDisabled). - The example also removes parent
SafeArea/Cardsurfaces when ads are disabled (not just the ad inside them). - Anchored adaptive uses a 50dp pre-load floor and then the exact loaded dimensions.
- Inline adaptive defaults to zero before load.
- Fixed and loaded sizing remain exact.
placeholderHeightremains supported, butadsDisabledalways overrides it (zero footprint).- Re-enabling ads reloads normally, with no duplicate requests.
- No public API change and no migration required.
Verification:
dart format/analyzeclean, 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).Release notes
Open source →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.
FIXED
- Remove-Ads now reclaims layout space. After
disableAds(), a mountedAdFlowBanner/AdFlowNativeAdpreviously 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 toadFlow.adsEnabled, so it happens on the framedisableAds()is called, without waiting for the asynchronous controller re-check; advanced controller mode collapses onAdBlocked(AdBlockReason.adsDisabled).adsDisabledoverrides any explicitplaceholderHeight. Re-enabling loads and renders normally again, with no duplicate loads, controller reminting or request storms. - Correct adaptive pre-load placeholder.
AdFlowBannerno longer reserves a speculative adaptive estimate (the old "15% of device height, clamped to 50–90dp"). A loaded banner always uses its exact livehandle.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 untilonAdLoaded). An explicitplaceholderHeightis still honoured for ordinary non-loaded states, andplaceholderHeight: 0opts into fully collapsed pre-load behaviour — but it is never honoured while ads are disabled.
EXAMPLE
- Remove-Ads now visibly removes the complete ad surfaces: the
bottomNavigationBarreturnsSizedBox.shrink()before constructingSafeArea(so no empty inset bar remains), and the nativeCard(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 withlaunchAndResume, so it no longer claims "never on a cold launch").
DOCS
- README: Remove-Ads collapses the ad widgets automatically; loaded banners use
exact SDK dimensions; anchored adaptive uses a 50dp pre-load floor then the
exact loaded size; inline adaptive defaults to zero before load;
placeholderHeight: 0for 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).
- Remove-Ads now immediately collapses Banner and Native surfaces to zero. Widget-first mode collapses synchronously on
-
5.1.020 Jul 2026Release notes
Open source →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.
App Open trigger modes
AppOpenConfig.triggerModeselectsAppOpenTriggerMode:resumeOnly— the default, exactly the v5.0 behavior (show on a genuine warm return only).launchOnly— show only at cold launch, viashowAtLaunchIfReady().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
falseat once and your app proceeds. It is one-shot per process launch, so afalseresult 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.Also in this release
- launch-only inventory retirement — after its single launch,
launchOnlystops maintaining an ad it can never show again. - Native-ad expiry —
NativeConfig.maxAdAge(safe default 55 min;nulldisables); stale native inventory is dropped and reloaded through the normal gate. - Runtime SSV readiness fix — a rewarded / rewarded-interstitial ad is never ready or showable until the latest SSV payload has settled (finalized before
AdLoaded; concurrent updates generation-serialized). - Callback / lifecycle containment —
AppStateEventNotifier.startListening()rejection contained;enableAds()/disableAds()and the App Open manager are inert afterdispose(). - Backward compatibility —
FullScreenAdControllerBase.onLoaded()unchanged;resumeOnlydefault unchanged. - Docs & example — README/MIGRATION/CHANGELOG reconciled; the example gains a real launch/loading screen demonstrating the launch path without an artificial delay.
See the CHANGELOG and MIGRATION. Live on pub.dev: https://pub.dev/packages/ad_flow/versions/5.1.0
Release notes
Open source →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.
NEW
- App Open trigger modes.
AppOpenConfig.triggerModeselectsAppOpenTriggerMode.{launchOnly, resumeOnly, launchAndResume}. Default isresumeOnly— the exact v5 behaviour. - Explicit cold-launch opportunity.
AppOpenAdManager.showAtLaunchIfReady()(reachable asads.appOpen.showAtLaunchIfReady()), forlaunchOnly/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 — returnsfalseimmediately otherwise. It is one-shot per process launch (survivingAdFlowreinitialization), so afalseresult 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;nulldisables). 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.
FIXED
- Runtime SSV readiness race. A rewarded / rewarded-interstitial ad could
become externally ready and showable carrying a stale (or missing)
server-side-verification payload: the ad published
AdLoadedbefore an in-flight override finished re-attaching, so a state listener or an immediateshow()used the previous payload; concurrent updates could also complete out of order. Now the loaded handle is finalized (the override settled) BEFOREAdLoadedis 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. - Uncontained
AppStateEventNotifier.startListening()rejection in the seam is now contained (was an unhandled zone error on a misconfigured host). enableAds()/disableAds()afterAdFlow.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 newAdBlockReasoncase, and the removal of the conceptually invaliddeferMediationInit.BREAKING
AdFlowConfig.deferMediationInitREMOVED (was in 4.0.0). It called the plugin'sdisableMediationInitialization, 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. UseforwardConsentinstead — it now runs before init (below).AdSdk.disableMediationInitialization()REMOVED from the seam interface (was in 4.0.0). It backeddeferMediationInitand has no correct use (see above). Affects only code that implements or subclassesAdSdkdirectly (a custom seam, or a test double not built on the shippedFakeAdSdk) — remove the override. Apps using the package normally never touchAdSdk.forwardConsentruns BEFOREMobileAds.initialize(). Mediation adapters initialize duringMobileAds.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, runsforwardConsent, 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.unsafeFailOpeninitializes/serves anyway. UI is never blocked —initialize()returns immediately; onlywhenReady/loads wait. Non-adopters keep parallel init.AdBlockReasongainedconsentNotForwarded— exhaustive switches overAdBlockReasonneed the new case (or a wildcard). Non-adopters offorwardConsentnever see it.
Added
forwardConsentonAdFlow.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.timeoutdoes 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(failCloseddefault,unsafeFailOpen= explicit unsafe opt-out).
Fixed (correctness / reward integrity / reliability)
- Frequency-cap late-hydration overwrite.
_hydratebounds 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. - Runtime SSV in-flight race + fail-drop.
setServerSideVerificationcalled 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. - Rewarded-interstitial: re-validate after the intro. The mandatory intro
is unbounded; a Remove-Ads purchase or the ad aging past
maxAdAgeduring it was ignored and the ad showed anyway.show()now re-checks live permission and expiry after the intro, rolling back rather than showing. - Async callback + refreshed-banner isolation.
guardedCallbacknow contains an ASYNC callback's later rejection (anonConsentChanged/onPaidEventasync 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. NewsafeUnawaitedcontains a rejecting handledispose()/subscriptioncancel()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,NativeConfigexactly-one.- Forwarder serialization across the timeout boundary.
Future.timeoutdoes 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 oneforwardConsentruns at a time, strictly ordered. - Stale-consent ad invalidation. After a consent/privacy mutation a warm
full-screen ad or visible banner/native (requested under the old consent)
is dropped and reloaded under the fresh gate; a full-screen ad on screen is
not interrupted. Handled by the existing
AdController.recheckGate()(which already runs after every consent mutation) via an internal consent-generation stamp — no new public method, no new integration step. MediationNetworkExtrasasserts against an empty class name (a silent reflection no-op at request time).
Docs
- README install constraint corrected
^3.0.0 → ^5.0.0; theAdBlockReasoncases (requestConfigNotApplied,internalError,consentNotForwarded) added to the enumeration; a "What's new" section covers the newer surfaces. doc/MEDIATION_SETUP.mddocumentsforwardConsentas the recommended fail-closed path,mediationConsentPolicy, and a concreteMediationNetworkExtrasexample.
-
5.0.020 Jul 2026Release notes
Open source →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-invaliddeferMediationInitis removed.📦 pub.dev: https://pub.dev/packages/ad_flow/versions/5.0.0
Breaking changes
- Removed
AdFlowConfig.deferMediationInitandAdSdk.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.AdSdkremoval affects only direct seam implementers;FakeAdSdk/GmaAdSdkare updated. forwardConsentruns BEFOREMobileAds.initialize()and fails CLOSED by default. A failed/timed-out forward means the GMA SDK is not initialized and mediation-capable loads block withAdBlockReason.consentNotForwarded(retried in the background); init + serving recover when forwarding succeeds. UI is never blocked —initialize()returns immediately. Opt out only viaMediationConsentFailurePolicy.unsafeFailOpen.AdBlockReasongainedconsentNotForwarded— exhaustive switches need the new case (or a wildcard).
Added
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.timeoutdoes not cancel its source) and generation-guarded.AdFlowConfig.mediationConsentPolicy+MediationConsentFailurePolicy(failCloseddefault,unsafeFailOpen= explicit unsafe opt-out).
Fixed (correctness / reward integrity / reliability)
- Frequency-cap late-hydration overwrite (late store read now merges, never rolls memory back / double-counts).
- Runtime SSV in-flight race + fail-drop (re-apply on install; drop a warm ad on attach failure).
- Rewarded-interstitial: re-validate live permission + expiry after the unbounded intro.
- Async-callback + refreshed-banner-paid isolation; safe teardown (
safeUnawaited). validate()mirrors every constructor assert (release builds strip asserts).- Forwarder serialization across the timeout boundary (at most one invocation, strictly ordered).
- Stale-consent ad invalidation folded into
recheckGate()via an internal consent-generation stamp (no new public method). MediationNetworkExtrasasserts against an empty class name.
Verification
Static/tests:
flutter analyzeclean · 494 tests · coverage 85.2% ·pana160/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 — known limitation (not a v5 blocker)
Unity mediation via
gma_mediation_unityis temporarily unavailable with 5.0.0: the latest published adapter (1.8.1) requiresgoogle_mobile_ads: ^8.0.0while 5.0.0 requires^9.0.0. Google'smainhas an in-progress1.9.0for^9.0.0but it is unpublished; this release intentionally uses no git dependency, override, or vendored patch. Documented as an optional-integration limitation indoc/MEDIATION_SETUP.md. Unity mediation itself was not runtime-validated.Upgrading
Most apps compile unchanged — see
MIGRATION.md. DeletedeferMediationInitif set; add theconsentNotForwardedcase to exhaustiveAdBlockReasonswitches; if you useforwardConsent, note it now runs before init and fails closed.Full notes:
CHANGELOG.md. - Removed
-
4.0.018 Jul 2026Release notes
Open source →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.Release notes
Open source →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.
BREAKING
AdBlockReasongained cases (requestConfigNotApplied,internalError) andAdFlowErrorKindgainedssv— exhaustive switches over these enums need the new cases.- SSV fail-closed. A rewarded / rewarded-interstitial load whose
configured server-side verification cannot be attached is now a FAILED
load (
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. - Rewarded interstitial: atomic show reservation. Every policy check
(consent, per-slot AND global caps, pacing, expiry, coordinator) now runs
BEFORE the mandatory intro, and the full-screen claim is held through it:
accepting the intro can no longer end in "no ad, no reward", nothing can
stack over the intro (app-open included), and a throwing intro presenter
rolls back instead of rejecting
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. - Request configuration is a retried process with a failure policy.
RequestConfigFailurePolicy {auto, failOpen, failClosed}onAdFlowConfig(defaultauto): whenupdateRequestConfigurationfails 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). AdGateconstructor:configReady(future) replaced bysettleRequestConfig(bounded callback).- Implementer note:
AdSdkgaineddisableMediationInitialization();FakeAdSdkgained knobs (ssvAttachError, dispatch counters,fullScreenRequests). Package-provided fakes are updated; externalAdSdkimplementations must add the new member.
Added
- Per-load watchdog —
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 atAdLoadingfor 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. - Per-slot
AdRequestOptionson every format config (keywords, contentUrl, nonPersonalizedAds, AdMob-adapter extras) plusMediationNetworkExtrasmapped 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.
Fixed (correctness / revenue / reward integrity)
- No collaborator or app-callback throw can wedge a controller. The
permission gate never throws (contained →
AdBlockReason.internalError, re-checked on backoff); the whole load body sits in one try; app callbacks (onPaidEvent,onAdBlocked, reward grants,onConsentChanged) are isolated viaFlutterError.reportError. A throwingcanRequestAds()used to pin slots atAdLoadingforever with an unhandled async error. - Indeterminate permission never drops a live ad — a transient channel hiccup during a gate re-check keeps the mounted banner/native earning; only definite answers (Remove-Ads, consent withdrawn) take ads down.
- Frequency caps are memory-authoritative. An impression recorded at dismiss binds the very next check from ANY controller (min-gap/hourly state used to be read back from storage mid-write — two full-screen ads could run back to back). Persistence is a serialized write-behind snapshot chain (no lost updates); a hanging/corrupt store degrades to session-only capping instead of blocking every show.
UmpConsentGateway.ensureCanRequestAdshonours its "never throws" contract on the finalcanRequestAds()read; Gma handles close their event streams even when the channel dispose rejects.
-
3.0.017 Jul 2026Release notes
Open source →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.Release notes
Open source →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.
BREAKING
AdBlocked(reason)is a newAdLoadStatecase. A load refused by policy (consent pending, Remove-Ads, withdrawal, disposed graph) now reports itself as a state instead of anAdIdleindistinguishable 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 toAdLoadingon its own.- Widget-first ad widgets.
AdFlowBanner(adFlow: ads)/AdFlowNativeAd(adFlow: ads)create AND own their controller, making the ADR-029 footgun (minting a controller insidebuild()→ 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 keepshow({onReward}).AdGateis a pure permission gate: the racy composedcanShow()query (review finding #6) and its caps/coordinator collaborators are removed. Show pacing lives in the controllers, where the atomictryEnter()is.AppOpenConfig.showOnColdStartremoved (deprecated + ignored since 2.1.0; it never could do anything). Banner/native slot constants renamedslot→slotNameto match the full-screen formats.
Added (3.0)
AdFlow.canRequestAds— aValueListenable<bool>with the LIVE consent answer: follows a late consent grant (ADR-035 retry) and a privacy-options withdrawal, unlike the one-shotwhenReadysnapshot.
Fixed (correctness / revenue)
- Banner refresh/resize races: a rotation during an in-flight opt-in
refresh could leak a live
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. - Refresh swaps now actually reach the screen: the plugin's
AdWidgetcannot 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/AdFlowNativeAdnow key the hosted subtree by handle identity, forcing a correct remount. - Inline-adaptive auto-refresh: a failed post-refresh size query tore
down the LIVE mounted banner and silently ended its revenue reporting; it
now only fails the initial load. A refresh that resolves a different
height updates the widget via the new
BannerHandle.dimensionslistenable (inline adaptive creatives vary per refresh). - Preloaded full-screen ads expire (Google documents ~1 hour): new
maxAdAgeon 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. - Ads come DOWN when no longer permitted:
disableAds()(Remove-Ads),dispose(), a re-initialize, and a consent withdrawal throughads.consentnow 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. NewAdController.recheckGate(). - show() no longer holds the shared coordinator across a consent settle:
a network-bound consent retry could freeze every full-screen format behind
one
show()call for up to 30s. The show path now uses cheap live checks only (AdGate.showBlockReason). - View-ad click latch can no longer be stranded: an iOS in-app overlay
click (open→close with no foreground event) used to eat the NEXT genuine
warm return's app-open ad.
onAdClosednow starts a 3s grace clock; Android's external-browser return ordering stays suppressed. - Raw platform-channel throws from the load dispatch are normalized to
AdFlowErrorand no longer leak the constructed ad + stream controllers. SharedPrefsKeyValueStorereads type-corrupt data as absent instead of throwing (a throwing cap read blocked every full-screen show, with no self-heal).- Consent/ATT primers wait (bounded) for the first frame, so a fast launch no longer silently drops the primer before the navigator mounts.
PrivacyOptionsButtonfailures default toFlutterError.reportErrorinstead of a silent swallow.
Added
- Runtime SSV:
setServerSideVerification(ssv)on both rewarded controllers — setuserIdafter login and per-showcustomData; applies to the warm ad and future loads; throws if attaching fails. - Mediation observability:
AdResponseSummary(handle.response/controller.response) — winning ad source, adapter class, response ID.AdPaidEventgainsslotandadSourceNamefor analytics-ready impression logging. AdFlowConfig.validate()(run automatically): empty ad-unit strings and nonsensical durations fail fast at init.- Null-safe slot getters:
interstitialOrNull,rewardedOrNull,rewardedInterstitialOrNull,appOpenOrNull,appOpenControllerOrNull. - Testing surface:
FakeBannerHandle.simulateResize/responseSummary,FakeFullScreenAdHandle.simulateShowFailed/ssvUpdates/ssvUpdateError,FakeAdSdk.onPrivacyOptionsFormShown.
Changed
AdFlow.consentnow 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.- Docs:
doc/MEDIATION_SETUP.mdanddoc/NATIVE_ADS_SETUP.mdrewritten 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.
For implementers of the seam interfaces (rare)
BannerHandlegaineddimensions; all handles gainedresponse; the rewarded handles gainedupdateServerSideVerification;AdControllergainedrecheckGate(). The in-package fakes implement all of these — custom implementations must add them. -
2.1.114 Jul 2026Release notes
Open source →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.
- README updated to 2.1.x (install snippet and both "Set up with AI" prompts still
told an assistant to pin ^2.0.0). - Documented the diagnostic surface: AdBlockReason, AdFlow.onAdBlocked,
controller.lastBlockReason. - Documented that the global frequency cap never blocks a user-initiated rewarded
ad (ADR-039), and the new RewardedConfig.cap / RewardedInterstitialConfig.cap. - Corrected a stale SKILL.md trap that would have led the next model to
re-introduce the app-open cold-start latch ADR-043 removed.
dart format clean (0 changed), analyze clean, 313 tests pass.
Co-Authored-By: Claude Opus 4.8 [email protected]
Release notes
Open source →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. - README updated to 2.1.x (install snippet and both "Set up with AI" prompts still
-
2.1.014 Jul 2026Release notes
Open source →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.
Revenue
- The global frequency cap no longer blocks user-initiated rewarded ads
(ADR-039). A user who tapped "watch an ad for 100 coins" 10s after an
interstitial fired got no ad, no reward and no explanation — the shipped
default global gap (15s) silently refused the highest-eCPM format in the
package. The global cap now paces involuntary ads only (interstitial,
app-open). Rewarded impressions are still recorded globally, so an
interstitial cannot fire straight after one.
New:
RewardedConfig.cap/RewardedInterstitialConfig.cap(unlimited by default) if you do want a per-format limit. - App-open ads now show on the FIRST genuine warm return of a session (ADR-043). The manager was consuming that return as a "cold start" the platform never actually emits — costing one impression in every single session, on both platforms. A true cold start still cannot show an ad: nothing is loaded yet.
- A banner refresh no longer blanks the slot (ADR-041). It used to destroy the live ad and reload from empty, so the slot went blank for the whole load — multi-second on a weak network, every cycle — and a refresh that merely failed (no-fill, routine) left it empty, having destroyed a perfectly good ad to get there. The replacement now loads in the background and swaps in only on success.
Defaults changed
BannerConfig.minRefreshnow defaults tonull= 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. PassminRefresh:explicitly to opt back in.- The frequency gap is now measured from the previous ad's DISMISS, not its SHOW (ADR-040). Stamped at show time, the gap ran down while the user was still watching: a 30s rewarded ad under a 15s global gap used the gap up on screen, so an interstitial could fire the instant the user closed it — two full-screen ads back to back.
AppOpenConfig.showOnColdStartis deprecated and ignored (ADR-043). It could never do what its name promised, and its only real effect is now the default. Remove it.
Policy
- An app-open ad no longer stacks on a banner/native ad (ADR-042). Returning
from a banner or native ad the user clicked no longer shows one — they were
being handed a second ad the moment they closed the first. And the new
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.
Robustness
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.
New
AdBlockReason+AdFlow.onAdBlocked+controller.lastBlockReason(ADR-045) — the answer to "why aren't my ads showing?". A refused load reported plainAdIdle, 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 newAdLoadStatecase:AdLoadStateis sealed, and adding one would break every exhaustiveswitchin every app.AdFlow.setBlockingViewAdVisible(bool);BannerAdController.revision,.resize(),.loadedWidth;StoredFrequencyCapPolicy.globalCapExemptSlots;AdGate.loadBlockReason();FullScreenAdCoordinator.noteViewAdOpened()/.consumeViewAdOpened()/.blockingViewAdVisible.
- The global frequency cap no longer blocks user-initiated rewarded ads
(ADR-039). A user who tapped "watch an ad for 100 coins" 10s after an
interstitial fired got no ad, no reward and no explanation — the shipped
default global gap (15s) silently refused the highest-eCPM format in the
package. The global cap now paces involuntary ads only (interstitial,
app-open). Rewarded impressions are still recorded globally, so an
interstitial cannot fire straight after one.
New:
-
2.0.114 Jul 2026Release notes
Open source →- Docs: added a "Set up with AI" README section with copy-paste new-setup and v1→v2 migration prompts. No code changes.
-
2.0.014 Jul 2026Release notes
Open source →Ground-up rewrite targeting
google_mobile_ads ^9.0.0. Breaking — see MIGRATION for the field-by-field and symbol-by-symbol mapping.- NEW: Rewarded interstitial format with the policy-mandated intro/skip
screen enforced by construction (
RewardedIntroScreen+ injected presenter). - NEW: Frequency capping — per-format time/count caps AND a global cross-format cap, persisted across restarts.
- NEW: Interstitial user-action pacing (
recordUserAction+minActionsBetween), opt-in by first use. - NEW: Server-side verification options for rewarded formats.
- NEW:
onPaidEventimpression-level revenue callback for every format. - NEW:
package:ad_flow/ad_flow_testing.dartshipsFakeAdSdkso apps can unit-test their ad integration. - NEW: Experimental Next-Gen GMA SDK opt-in on Android via
--dart-define=USE_NEXT_GEN_SDK=true(no Dart changes). - NEW: Non-blocking
AdFlow.initialize()— builds the graph synchronously and returns immediately; consent/ATT/SDK-init run in the background. Render your first frame at once (noFutureBuilder<AdFlow>spinner). OptionalFuture<bool> ads.whenReadyawaits the consent gate. Nothing loads before the gate opens (ADR-032). - NEW: Opt-in consent & ATT priming screens — the v2 equivalent of v1's
initializeWithExplainer, now decoupled fromBuildContextvia presenters (attExplainer/consentExplaineroninitialize, ready-madeAttExplainerScreen/ConsentExplainerScreen). SupplyingattExplainerenables client-driven ATT (iOS). Additive — pass nothing for today's UMP-driven behaviour (ADR-030). - IMPROVED: Architecture — dependency injection everywhere, no static
global config; one
AdSdkseam is the only door to the plugin; state isValueListenable<AdLoadState>. - IMPROVED: Consent — UMP wrapped once into
ConsentGatewayFutures; ATT handled by UMP (dependency onapp_tracking_transparencyremoved); consent failures degrade gracefully with a typedlastError. - IMPROVED: Retries — exponential backoff with jitter, cooldown, then automatic re-arm (v1 never re-armed banner/native loads).
- FIXED: App-open ads no longer fire after Control Center / permission
dialogs / app switcher (v1 treated iOS
inactiveas backgrounding); foreground detection now usesAppStateEventNotifier; 4-hour expiry enforced with discard-and-reload. - FIXED:
isUsingTestAdsfalse positives — test mode is an explicit config flag, never derived from resolved IDs. - BREAKING: Requires Flutter ≥ 3.38.1, Dart ≥ 3.10, iOS 13+, Android minSdk 24 / compileSdk 36 (from google_mobile_ads 9.x).
- BREAKING: All v1 managers, mixins, Easy* widgets and the broad
google_mobile_adsre-export are gone — see MIGRATION §7.
- NEW: Rewarded interstitial format with the policy-mandated intro/skip
screen enforced by construction (
-
1.3.1818 Feb 2026Release notes
Open source →- NEW:
EasyBannerAdnow supports optionalSafeAreawrapping (#6)- Added
useSafeAreaparameter (default:true) to prevent extra black space - Set to
falsewhen the banner is already inside aSafeAreaorScaffoldthat handles insets - Works for fixed-size, adaptive, and collapsible banners
- Added
- IMPROVED: Extracted
_wrapWithSafeArea()helper inEasyBannerAdfor cleaner SafeArea logic - IMPROVED: Test suite expanded to 1035 tests
- IMPROVED: Branch protection enabled on
main(requires PR review before merge)
- NEW:
-
1.3.1711 Feb 2026 -
1.3.1611 Feb 2026Release notes
Open source →- FIX: App Open ad no longer shows immediately after closing an interstitial or rewarded ad
- OS lifecycle (
paused → resumed) from fullscreen ad overlays was mistaken for a real foreground event - Added fullscreen-ad suppression with 5-second grace period in
AppLifecycleReactor InterstitialAdManagerandRewardedAdManagernow signal showing/dismiss to the reactor
- OS lifecycle (
- IMPROVED: Example launcher now distinguishes "initialized" from "can request ads"
- Shows 3 states: Initializing, Initialized (No Consent), AdFlow Ready
- No longer shows "Initializing…" forever when consent is denied
- IMPROVED: Test suite expanded to 1031 tests
- FIX: App Open ad no longer shows immediately after closing an interstitial or rewarded ad
-
1.3.1511 Feb 2026Release notes
Open source →- IMPROVED: Comprehensive README rewrite with step-by-step integration guide
- Added callbacks reference tables for all 5 ad types
- Added status listeners documentation
- Added
ignoreCooldowninterstitial example
- IMPROVED: Restructured example app with focused per-ad-type demos
- Launcher menu with navigation to Banner, Interstitial, Rewarded, Native, App Open examples
- All-in-one demo page retained for quick overview
- IMPROVED: Added CI/CD with GitHub Actions
- Automated format, analyze, and test on push/PR
- Auto-publish to pub.dev on version tag push
- IMPROVED: Expanded test suite to 1015 tests
- INTERNAL: Added
AdSdkabstraction andAdManagerMixinfor testability - INTERNAL: Added
PrivacyRequirementMixinfor consent checks
- IMPROVED: Comprehensive README rewrite with step-by-step integration guide
-
1.3.1404 Feb 2026Release notes
Open source →- NEW: Non-blocking initialization for instant app startup
- App can start immediately without waiting for AdFlow to initialize
- Ads load in background while users interact with the app
- Dramatically improves user experience on slow networks
- NEW:
waitForInit()method - waits for initialization to complete- Returns
Future<bool>indicating if ads can be requested - Returns immediately if already initialized
- Use for fullscreen ads (interstitial, rewarded) before showing
- Returns
- NEW:
initStream- broadcast stream that emits when initialization completes- Widgets can subscribe and react when AdFlow becomes ready
- Useful for complex scenarios requiring custom ad loading
- IMPROVED:
EasyBannerAdandEasyNativeAdare now fully reactive- Automatically subscribe to
initStreamon mount - Auto-load ads when AdFlow initialization completes
- No code changes required - existing widgets work seamlessly
- Automatically subscribe to
- IMPROVED: Test coverage expanded from 309 to 328 tests
- Added tests for
waitForInit()behavior - Added tests for reactive widget initialization
- Added tests for stream subscription cleanup
- Added tests for
- NEW: Non-blocking initialization for instant app startup
-
1.3.1304 Feb 2026Release notes
Open source →- FIX: Ad managers now properly guard against dispose-during-retry crashes
- Added
_isDisposedflag toInterstitialAdManager,RewardedAdManager,AppOpenAdManager,NativeAdManager - Retry loops now exit early if manager is disposed mid-operation
- Prevents
setState() called after dispose()errors in edge cases
- Added
- FIX: Status listener iteration is now safe from concurrent modification
- All ad managers now use
List.of()when notifying listeners - Prevents
ConcurrentModificationErrorif listener removes itself during callback
- All ad managers now use
- FIX: Removed unnecessary
metaimport inad_service.dart - IMPROVED: Test coverage expanded from 227 to 309 tests
- Added comprehensive tests for
EasyPrivacySettingsButtonandPrivacySettingsListTile - Added dispose guard tests for all ad managers
- Added listener safety tests for concurrent modification scenarios
- Added comprehensive tests for
- FIX: Ad managers now properly guard against dispose-during-retry crashes
-
1.3.1203 Feb 2026Release notes
Open source →- FIX: Splash screen remains too long when AdMob initialization is slow (#4)
- Added smart timeouts with sensible defaults to prevent indefinite blocking
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 loading- Consent dialogs are NOT affected - they always wait for user interaction (compliance)
- Zero code changes required - existing apps get faster initialization automatically
- IMPROVED: Background retry for SDK initialization if timeout occurs
- IMPROVED: Cold-start app open ads now use bounded timeout instead of blocking indefinitely
- FIX: Splash screen remains too long when AdMob initialization is slow (#4)
-
1.3.1108 Jan 2026Release notes
Open source →- FIX:
AppOpenAdManager.addStatusListenercallback now fires correctly (#3)- Status listeners were not notified when using
showAdIfAvailable() - Now properly calls
_notifyStatusListeners()on show/dismiss/fail events
- Status listeners were not notified when using
- FIX: iOS App Store rejection for GDPR shown after ATT denial (#2)
- Added
skipGdprConsentIfAttDeniedconfig option (default:true) - When user selects "Ask App Not to Track", GDPR consent UI is skipped
- Prevents Apple Guideline 5.1.1 rejections
- Set to
falseif you legally require showing GDPR consent regardless of ATT
- Added
- NEW:
ConsentManager.lastAttStatusandisAttDeniedgetters- Access the iOS ATT authorization status after consent gathering
- FIX:
-
1.3.1008 Jan 2026Release notes
Open source →- NEW:
EasyBannerAdnow supports custom ad sizes- Use
EasyBannerAd(adSize: AdSize.mediumRectangle)for fixed-size banners - Supports all standard sizes:
banner,largeBanner,mediumRectangle,leaderboard, etc. - Fixed-size banners skip orientation handling for better performance
- Priority:
adSize>collapsible> adaptive (default)
- Use
- NEW:
-
1.3.906 Jan 2026Release notes
Open source →- FIX: Export
BannerAdListenerfromgoogle_mobile_ads(fixes #1)- Allows users to create custom-sized
BannerAdinstances directly
- Allows users to create custom-sized
- NEW: Added
BannerAdManager.loadBanner()method for custom ad sizes- Load banners with specific sizes like
AdSize.mediumRectangle(300x250) for dialogs - Same consent/disabled checks and callbacks as
loadAdaptiveBanner()
- Load banners with specific sizes like
- FIX: Export
-
1.3.806 Jan 2026Release notes
Open source →- NEW: Mediation support for third-party ad networks
- Added
MediationHelperclass for forwarding consent to mediation networks - Built-in support for Unity Ads and AppLovin with convenience methods
- Register custom adapters for any mediation network
- Consent auto-forwarded during
initialize()/initializeWithExplainer() - See
doc/MEDIATION_SETUP.mdfor complete integration guide
- Added
- DOCS: Added comprehensive mediation documentation
- IMPROVED: Updated copilot-instructions.md with mediation patterns
- NEW: Mediation support for third-party ad networks
-
1.3.703 Jan 2026Release notes
Open source →- FIX:
NativeAdWidgetnow respectsAdsEnabledManager.isDisabledon initial build - IMPROVED: Added comprehensive tests for
EasyNativeAdandNativeAdWidgetads-disabled behavior
- FIX:
-
1.3.603 Jan 2026Release notes
Open source →- NEW:
EasyNativeAdnow collapses when ads fail to load (no more empty white space)- Added
hideOnLoadingparameter (default:true) - collapses while loading - Added
hideOnErrorparameter (default:true) - collapses on load failure (e.g., no fill) - Set to
falseto show loading/error widgets with reserved height
- Added
- FIX: Removed double semicolon in
BannerAdManagercausing static analysis warning - IMPROVED: Better UX for fixed-height layouts like
bottomNavigationBar
- NEW:
-
1.3.530 Dec 2025Release notes
Open source →- FIX: All ad managers now respect
AdsEnabledManager.isDisabledstateloadAd()andshowAd()check disabled state before proceeding- Fixes race condition where
disableAds()inonCompletewas too late - Affected managers:
BannerAdManager,InterstitialAdManager,RewardedAdManager,AppOpenAdManager,NativeAdManager
- DOCS: Updated copilot-instructions.md with timing warning for disabling ads
- FIX: All ad managers now respect
-
1.3.426 Dec 2025Release notes
Open source →- FIX: Applied
dart formatto all files for pub.dev static analysis compliance
- FIX: Applied
-
1.3.326 Dec 2025Release notes
Open source →- IMPROVED: Code quality improvements across all ad managers
- Extracted magic numbers to named constants for better maintainability
- Added explicit types for improved type safety in
AdFlowConfig - Fixed potential memory leaks in dispose methods (banner, interstitial, app open)
- IMPROVED: Selective ad type preloading
preloadAds()now only preloads ad types that have real IDs configured- Added
hasBannerConfigured,hasInterstitialConfigured, etc. getters - Use only the ad types you need without loading unnecessary ads
- FIX:
reset()now properly callsAdFlowConfig.resetCurrent()- Previously config state persisted after reset, now fully resets
- FIX: Status listeners properly cleaned up in dispose methods
- IMPROVED: Simplified example files
- Replaced complex demo pages with two clean, reactive examples
example_with_explainer.dart- GDPR-friendly with explainer dialogexample_without_explainer.dart- Direct initialization- Both examples demonstrate reactive UI with status listeners
- IMPROVED: Code quality improvements across all ad managers
-
1.3.125 Dec 2025Release notes
Open source →- NEW: Added
AdFlow.instance.reset()for testing- Enables proper unit testing of singleton state
- Clears all managers and resets initialization
- FIX: Fixed barrel export to use correct file (
ad_service.dart) - FIX: Fixed
use_build_context_synchronouslywarnings inBannerAdManager - IMPROVED: Added lazy initialization for ad managers
- Managers only created when first accessed
- Better memory efficiency for apps using subset of ad types
- IMPROVED: Expanded test coverage from 140 to 185 tests
- Added
AdFlowsingleton tests - Added
EasyBannerAdwidget tests - Added
ConsentManagertests
- Added
- Removed duplicate
ad_flow_service.dartfile
- NEW: Added
-
1.3.025 Dec 2025Release notes
Open source →- NEW: Added
EasyPrivacySettingsButtonwidget for GDPR compliance- Auto shows/hides based on privacy options requirement
- Opens official Google privacy options form
- Customizable text, icon, and style
- NEW: Added
PrivacySettingsListTilefor settings screens - FIX:
initializeWithExplainer()now properly checks AdsEnabledManager- Previously skipped "Remove Ads" check, now matches
initialize()behavior
- Previously skipped "Remove Ads" check, now matches
- FIX:
isPrivacyOptionsRequired()now returns correct cached value- Was incorrectly returning
canRequestAdsinstead of privacy options status
- Was incorrectly returning
- Added production example with complete implementation guide
- Updated documentation with privacy button usage examples
- NEW: Added
-
1.0.224 Dec 2025 -
1.0.124 Dec 2025Release notes
Open source →- Initial release
- Banner ads (adaptive and collapsible)
- Interstitial ads with cooldown management
- App open ads with lifecycle handling
- Native ads with factory support
- GDPR/ATT consent management via UMP SDK
- iOS App Tracking Transparency support
- Remove Ads feature with persistence
- Multi-language consent dialogs (English, Spanish, Persian)
-
1.0.1+124 Dec 2025 -
1.0.0+124 Dec 2025Nothing published for this version
-
1.0.0+224 Dec 2025Nothing published for this version