adflow_flutter 2.2.0
adflow_flutter: ^2.2.0 copied to clipboard
Flutter plugin for the AdFlow ad-mediation library on top of AdMob: Interstitial, App Open, Rewarded, Native, and Banner ads. Android only for now.
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.