Placement Events
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
| Callback | Signature | Purpose |
|---|---|---|
onReady | void Function() | An ad is loaded and will display |
onFailed | void Function(TeadsAdError) | The request or render failed |
onClicked | void Function(TeadsAdClick) | A paid ad was clicked |
onClickedOrganic | void Function(TeadsAdClick) | Organic content was clicked |
onEvent | void Function(TeadsAdEvent, Map<String, dynamic>?) | Catch-all for every event |
onClosed | void 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.
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:
| Value | Fires when |
|---|---|
ready | An ad is available to display |
rendered | The creative has been rendered |
viewed | The ad met the viewability threshold |
failed | The request or render failed |
clicked | A paid ad was clicked |
clickedOrganic | Organic content was clicked |
play | Video playback started or resumed |
pause | Video playback paused |
loaded | The ad finished loading |
complete | Video playback reached the end |
startPlayAudio | Audio started (the user unmuted) |
stopPlayAudio | Audio stopped |
heightUpdated | The creative reported a new height |
willPresent | Interstitial — about to present |
presented | Interstitial — presented fullscreen |
willDismiss | Interstitial — about to dismiss |
dismissed | Interstitial — dismissed; the ad is spent |
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:
| Callback | Signature | Purpose |
|---|---|---|
onEvent | void Function(TeadsAdEvent event, Map<String, dynamic>? data) | Lifecycle and telemetry, sharing the typed TeadsAdEvent vocabulary |
onUnsupported | void Function(TeadsAdError error) | Parity gaps and page-reported errors |
onOrganicClick | void Function(Uri organicUrl) | An organic recommendation was tapped |
onFullScreenChange | void 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.
Collapse siblings with Visibility(maintainState: true) or a zero height. Removing one
re-parents the WebView and it stops painting.
onUnsupported
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
IgnorePointerorAbsorbPointeris 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
- Settings & Configuration — every config field
- Integration Guide — complete examples
- Troubleshooting Guide — common issues
Pro tip: wire onFailed and — for Feed — onUnsupported up from the start. They
are the difference between a diagnosable integration and a silent one.