adflow_flutter
Android-only Flutter bridge for AdFlow v2. Placements are declared once, ad lifecycle is exposed as reactive state, and banner/native platform views manage loading and rebinding themselves.
Setup
Add the package:
flutter pub add adflow_flutter
See https://pub.dev/packages/adflow_flutter for the full version list.
Set Android minSdk to 24, and add JitPack to the app's repositories:
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
Add the AdMob app ID to android/app/src/main/AndroidManifest.xml:
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-xxxxxxxxxxxxxxxx~yyyyyyyyyy" />
Initialize
Declare every placement in one place. Ad unit lists are tried as a waterfall.
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await AdFlow.initialize(
placements: const [
InterstitialPlacement(
'splash_interstitial',
adUnits: ['ca-app-pub-xxxxxxxxxxxxxxxx/yyyyyyyyyy'],
preload: false,
),
InterstitialPlacement(
'global_interstitial',
adUnits: ['ca-app-pub-xxxxxxxxxxxxxxxx/yyyyyyyyyy'],
),
AppOpenPlacement(
'app_open',
adUnits: ['ca-app-pub-xxxxxxxxxxxxxxxx/yyyyyyyyyy'],
autoShowOnForeground: true,
),
RewardedPlacement(
'rewarded',
adUnits: ['ca-app-pub-xxxxxxxxxxxxxxxx/yyyyyyyyyy'],
),
BannerPlacement(
'home_banner',
adUnits: ['ca-app-pub-xxxxxxxxxxxxxxxx/yyyyyyyyyy'],
),
NativePlacement(
'home_native',
adUnits: ['ca-app-pub-xxxxxxxxxxxxxxxx/yyyyyyyyyy'],
rendererId: 'medium',
),
],
);
runApp(const MyApp());
}
Use showInterval, useLogcatLogger, consentDebugGeography, and
consentDebugTestDeviceHashedIds on initialize when those settings are needed. Consent debug
settings must only be used for test devices.
Full-screen ads
Every handle exposes a stable ValueListenable<AdState>. awaitReady starts an idempotent load
and returns on loaded, failed, or timeout, so splash screens do not need polling.
final ad = AdFlow.interstitial('splash_interstitial');
final state = await ad.awaitReady(const Duration(seconds: 8));
if (state is AdLoaded) {
await ad.show(
onDismissed: continueNavigation,
onFailedToShow: (error) => continueNavigation(),
onBlocked: (reason) => continueNavigation(),
);
}
await AdFlow.rewarded('rewarded').show(
onUserEarnedReward: (reward) => grantReward(reward.amount),
);
States are AdIdle, AdLoading, AdLoaded, AdFailed, and AdShowing. Block reasons distinguish
loading, no-fill, consent, rule rejection, interval throttling, and another full-screen ad.
canShow() answers "would show() actually proceed right now" without side effects (no load, no
slot claim, no ad consumed) - checks showRule, the minimum interval between shows, and whether
another full-screen ad is currently showing, in addition to whether the ad is loaded. Useful for
gating a button before committing to show(), e.g. confirming with the user first:
final interstitial = AdFlow.interstitial('global_interstitial');
if (await interstitial.canShow()) {
await interstitial.show();
}
Not available on Banner/Native - those ad types have no such gates to check.
Banner and native widgets
The widgets react to placement state. No readiness polling, generation keys, or manual platform view recreation is required.
AdFlowBanner(
'home_banner',
loading: (_) => const SizedBox(height: 50, child: LinearProgressIndicator()),
failed: (_, error) => const SizedBox.shrink(),
)
AdFlowNative(
'home_native',
height: 250,
rendererId: 'medium',
loading: (_) => const SizedBox(height: 250, child: LinearProgressIndicator()),
failed: (_, error) => const SizedBox.shrink(),
)
await AdFlow.native('home_native').reload();
The Android AdFlowBannerView and AdFlowNativeAdView own attach, load, collapse, and rebind
behavior. A successful native reload is reflected without changing the Flutter widget key.
loading/failed build widgets during Flutter's build phase - calling setState() inside them
throws ("setState() or markNeedsBuild() called during build"). For side effects (updating other
state, logging, analytics...), use onLoading/onLoaded/onError instead - they always run after
the current frame finishes building, so setState() inside them is safe:
AdFlowNative(
'home_native',
loading: (_) => const SizedBox(height: 250, child: LinearProgressIndicator()),
failed: (_, error) => const SizedBox.shrink(),
onLoading: () => setState(() => _nativeStatus = 'loading'),
onLoaded: () => setState(() => _nativeStatus = 'loaded'),
onError: (error) => setState(() => _nativeStatus = 'error: ${error.message}'),
)
onLoading covers both "not yet requested" and "a load is in flight" (matches what the loading
builder shows). Same three callbacks are available on AdFlowBanner.
Collapsible native → banner
AdFlowCollapsibleNative shows a native ad with a close button; tapping it (or the native ad
failing to load) switches the same slot to a banner ad instead of leaving it empty:
AdFlowCollapsibleNative(
nativePlacementId: 'home_native',
bannerPlacementId: 'home_banner',
onCollapse: (reason) => log('collapsed: $reason'),
)
It composes AdFlowNative/AdFlowBanner directly - no separate platform view. Once native loads,
the banner is preloaded in the background after bannerPreloadDelay (default 2s) so it's ready
the moment the user taps close, without a visible loading gap. nativeLoading/nativeFailed/
bannerLoading/bannerFailed and their onNative*/onBanner* side-effect counterparts mirror
AdFlowNative/AdFlowBanner's loading/failed/onLoading/onLoaded/onError, scoped to
whichever ad is currently active. collapseIcon overrides the default close affordance; the
package has no Material/Cupertino dependency, so the default is drawn with plain widgets.
Enabling and disabling placements, and consent
final enabled = !isPremium;
AdFlow.interstitial('global_interstitial').setEnabled(enabled);
AdFlow.banner('home_banner').setEnabled(enabled);
// rewarded left out on purpose - the user opts in to watch it for a reward.
final error = await AdFlow.requestConsentIfNeeded();
final requirement = await AdFlow.getPrivacyOptionsRequirement();
if (requirement == PrivacyOptionsRequirement.required) {
await AdFlow.showPrivacyOptionsForm();
}
AdFlow.addRevenueLogger((event) {
// Forward event to analytics or attribution.
});
setEnabled(false) on a handle gates loading and showing for that placement only - other
placements are unaffected, so a bulk "premium" toggle can selectively exclude some (e.g. rewarded).
Re-enabling triggers a new demand-driven load for that placement.
Custom native renderer
Implement native v2's NativeAdRenderer in the Android app:
class CompactRenderer : NativeAdRenderer {
override fun onCreateView(context: Context, parent: ViewGroup): View =
NativeAdView(context) // Add and register asset views here.
override fun onBind(view: View, assets: NativeAdAssets) {
// Bind headline, body, CTA, media, and other assets.
}
}
Register it after super.configureFlutterEngine and use the same ID in a NativePlacement or
AdFlowNative widget:
AdflowFlutterPlugin.registerNativeAdRenderer(flutterEngine, "compact", CompactRenderer())
Unknown renderer IDs fall back to DefaultMediumNativeAdRenderer with a Logcat warning.
See example/ for a complete integration using Google test ad unit IDs.