Skip to main content

Placement Events

info

Media, Banner and Interstitial share one typed event vocabulary. Feed has its own callbacks, because it speaks the bridge protocol rather than a native placement's event stream. Both are covered here.

Media, Banner and Interstitial​

The callbacks​

CallbackSignaturePurpose
onReadyvoid Function()An ad is loaded and will display
onFailedvoid Function(TeadsAdError)The request or render failed
onClickedvoid Function(TeadsAdClick)A paid ad was clicked
onClickedOrganicvoid Function(TeadsAdClick)Organic content was clicked
onEventvoid Function(TeadsAdEvent, Map<String, dynamic>?)Catch-all for every event
onClosedvoid Function()Interstitial only — dismissed

Every callback is optional. Implement only what you need.

For MediaPlacement and BannerPlacement these are constructor arguments; for InterstitialPlacement they are assignable fields, because it is not a widget:

// Widget placements
MediaPlacement(
config: config,
onReady: () => debugPrint('ready'),
onFailed: (error) => debugPrint(error.reason),
);

// Interstitial
final interstitial = await InterstitialPlacement.create(config: config);
interstitial.onReady = () => debugPrint('ready');
interstitial.onClosed = () => debugPrint('dismissed');

TeadsAdError​

class TeadsAdError {
final String reason;
}
onFailed: (TeadsAdError error) {
debugPrint('Ad failed: ${error.reason}');
analytics.track('ad_error', {'reason': error.reason});
}

A failure is not exceptional — no-fill is a normal outcome. Leave your layout in a sensible state rather than showing an error to the user.

TeadsAdClick​

class TeadsAdClick {
final String? url;
}

url is null for a plain click on platforms and placements that do not report a click-through URL, so always null-check it.

Do not open the URL yourself

The SDK opens the click-through. Opening click.url as well makes the browser open twice. Use these callbacks for analytics only.

The exception is FeedPlacement.onOrganicClick, which exists precisely so you can handle navigation — see below.

TeadsAdEvent​

The catch-all onEvent receives every event, including those with a dedicated callback:

onEvent: (TeadsAdEvent event, Map<String, dynamic>? data) {
switch (event) {
case TeadsAdEvent.viewed:
analytics.track('ad_viewable');
case TeadsAdEvent.complete:
analytics.track('ad_video_complete');
default:
break;
}
}

Full enum:

ValueFires when
readyAn ad is available to display
renderedThe creative has been rendered
viewedThe ad met the viewability threshold
failedThe request or render failed
clickedA paid ad was clicked
clickedOrganicOrganic content was clicked
playVideo playback started or resumed
pauseVideo playback paused
loadedThe ad finished loading
completeVideo playback reached the end
startPlayAudioAudio started (the user unmuted)
stopPlayAudioAudio stopped
heightUpdatedThe creative reported a new height
willPresentInterstitial — about to present
presentedInterstitial — presented fullscreen
willDismissInterstitial — about to dismiss
dismissedInterstitial — dismissed; the ad is spent
Not every placement emits every event

Do not assume the whole enum applies. MediaPlacement never emits loaded or rendered; the fullscreen events come only from Interstitial. Unknown native event names are tolerated rather than throwing, so a future native event will not crash your app — but it will not appear in the enum either.

heightUpdated is handled internally for sizing. You do not need to react to it — the widgets resize themselves.

Feed​

Feed's callbacks differ in shape and purpose:

CallbackSignaturePurpose
onEventvoid Function(TeadsAdEvent event, Map<String, dynamic>? data)Lifecycle and telemetry, sharing the typed TeadsAdEvent vocabulary
onUnsupportedvoid Function(TeadsAdError error)Parity gaps and page-reported errors
onOrganicClickvoid Function(Uri organicUrl)An organic recommendation was tapped
onFullScreenChangevoid Function(bool fullScreen)The page wants to enter or leave fullscreen

onOrganicClick​

Organic recommendations point at your content, so the SDK hands you the URL instead of opening a browser over your app:

FeedPlacement(
config: feedConfig,
onOrganicClick: (Uri organicUrl) {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => ArticleScreen(url: organicUrl.toString())),
);
},
)

Leave it null and the article opens in an external browser instead. Either way the click is registered first — that does not depend on this callback.

onFullScreenChange​

The placement can only grow its own slot, so your app must fold its chrome away. See the Integration Guide for a complete example.

Never remove widgets from the tree while fullscreen

Collapse siblings with Visibility(maintainState: true) or a zero height. Removing one re-parents the WebView and it stops painting.

onUnsupported​

This is the only route for Feed errors

The plugin logs nothing of its own for parity gaps or page-reported errors. A build that does not wire onUnsupported up sees a missing feature as silence. Wire it up in any build you intend to debug.

FeedPlacement(
config: feedConfig,
onUnsupported: (TeadsAdError error) {
debugPrint('Feed unsupported: ${error.reason}');
analytics.track('feed_unsupported', {'reason': error.reason});
},
)

onEvent​

Shares the typed TeadsAdEvent vocabulary, mapped from the bridge page's events (with fill/no-fill mapped onto TeadsAdEvent.ready/TeadsAdEvent.failed):

FeedPlacement(
config: feedConfig,
onEvent: (TeadsAdEvent event, Map<String, dynamic>? data) {
debugPrint('Feed event: ${event.identifier} — $data');
},
)

Complete Example​

class _ArticleScreenState extends State<ArticleScreen> {
bool _fullScreen = false;


Widget build(BuildContext context) {
return Scaffold(
appBar: _fullScreen ? null : AppBar(title: const Text('Article')),
body: ListView(
physics: _fullScreen ? const NeverScrollableScrollPhysics() : null,
children: [
Visibility(
visible: !_fullScreen,
maintainState: true,
child: Column(
children: [
const Text('Article content…'),
MediaPlacement(
config: MediaPlacementConfig(
pid: 84242,
articleUrl: widget.articleUrl,
),
onReady: () => analytics.track('media_ready'),
onFailed: (error) =>
analytics.track('media_failed', {'reason': error.reason}),
onClicked: (click) =>
analytics.track('media_clicked', {'url': click.url}),
onEvent: (event, data) {
if (event == TeadsAdEvent.viewed) {
analytics.track('media_viewable');
}
},
),
],
),
),
FeedPlacement(
config: FeedPlacementConfig(
widgetId: 'MB_1',
installationKey: 'NANOWDGT01',
articleUrl: widget.articleUrl,
),
onOrganicClick: (url) => Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => ArticleScreen(articleUrl: url.toString()),
),
),
onUnsupported: (error) => debugPrint('Feed: ${error.reason}'),
onFullScreenChange: (fullScreen) =>
setState(() => _fullScreen = fullScreen),
),
],
),
);
}
}

Best Practices​

Keep callbacks cheap​

They run on the platform-channel response path. Heavy synchronous work there shows up as jank while an ad is loading. Queue it instead.

Guard setState​

An event can arrive after the widget is unmounted:

onReady: () {
if (!mounted) return;
setState(() => _adReady = true);
}

Do not re-create callbacks needlessly​

Building a new closure on every rebuild is harmless for correctness but makes the placement's config identity churn. Prefer instance methods over inline closures in a frequently-rebuilt widget.

Treat failure as normal​

No-fill is an expected outcome, not an error state to surface to users. Collapse the slot gracefully.

Troubleshooting​

Events not firing​

  • Confirm TeadsSdk.configure() completed without throwing
  • Confirm the callback is actually passed to the constructor
  • Confirm the widget is mounted and on screen
  • Raise the log level: TeadsLog.level = LogLevel.debug

Clicks not working​

  • Check for an overlay — even a transparent one — over the placement
  • Check that no ancestor IgnorePointer or AbsorbPointer is swallowing the gesture
  • Test on a real device

Feed fullscreen breaks the layout​

You are almost certainly removing widgets from the tree rather than collapsing them.

Next Steps​


tip

Pro tip: wire onFailed and — for Feed — onUnsupported up from the start. They are the difference between a diagnosable integration and a silent one.