Feed Placement
Feed (SmartFeed) is a ready-made, self-paginating content recommendations widget. Drop FeedPlacement into a scrolling list and it handles the rest.
The plugin's example app, shipped with the teads_flutter package, includes a working screen for this format. Use it as a reference while you set up this placement.
Example
FeedPlacement(
config: FeedPlacementConfig(
widgetId: 'MB_1', // Provided by your Teads account manager
installationKey: 'YOUR_INSTALLATION_KEY',
articleUrl: 'https://example.com/article/123',
),
onOrganicClick: (Uri organicUrl) {
// Open your own article — see Organic clicks below.
},
onUnsupported: (TeadsAdError error) => debugPrint('Feed: ${error.reason}'),
)
Pagination is automatic
As the user scrolls to within 600 logical pixels of the end, the placement requests the next chunk and grows to fit it, until the feed is exhausted. There is nothing to call and no "load more" control to build.
By default the feed grows to its full content height. maxHeight caps it so the feed scrolls internally instead; leaving it null matches native behavior.
FeedPlacement(config: feedConfig, maxHeight: 1200)
Multiple feeds on one page
widgetIndex is a coordination key, not a positionOnly a placement with widgetIndex: 0 fetches its ad directly. Any other value waits for a widgetIndex: 0 placement on the same page to publish shared bridge parameters before it fetches at all.
If no widgetIndex: 0 placement is guaranteed to load on the same page, the ad never loads, and no error is reported. This mirrors the native SDKs exactly. Use widgetIndex: 0 unless you have another Feed guaranteed to load first with widgetIndex: 0.
ListView(
children: [
FeedPlacement(
config: FeedPlacementConfig(
widgetId: 'MB_1',
installationKey: 'YOUR_INSTALLATION_KEY',
articleUrl: articleUrl,
widgetIndex: 0, // fetches directly
),
),
const Text('More content…'),
FeedPlacement(
config: FeedPlacementConfig(
widgetId: 'MB_2',
installationKey: 'YOUR_INSTALLATION_KEY',
articleUrl: articleUrl,
widgetIndex: 1, // waits for index 0
),
),
],
)
Organic clicks
Organic recommendations point at your content, not an advertiser's, so the SDK hands you the URL instead of opening a browser over your app. Wire onOrganicClick up to keep the reader inside your own UI; leave it null and the article opens in an external browser. The click is registered either way.
FeedPlacement(
config: feedConfig,
onOrganicClick: (Uri organicUrl) {
Navigator.of(context).push(
MaterialPageRoute(builder: (_) => ArticleScreen(url: organicUrl.toString())),
);
},
)
Fullscreen video
The feed can ask to go fullscreen for a video card. It can only grow its own slot — it cannot paint over your app bar or the system bars — so your app must fold its chrome away when asked.
import 'package:flutter/services.dart'; // for SystemChrome
class _ArticleScreenState extends State<ArticleScreen> {
bool _fullScreen = false;
void dispose() {
// Leaving mid-fullscreen would otherwise strand the app in immersive mode.
SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
super.dispose();
}
Widget build(BuildContext context) {
return Scaffold(
appBar: _fullScreen ? null : AppBar(title: const Text('Article')),
body: ListView(
physics: _fullScreen ? const NeverScrollableScrollPhysics() : null,
children: [
// Collapse siblings, do NOT remove them from the tree.
Visibility(
visible: !_fullScreen,
maintainState: true,
child: const Text('Article content…'),
),
FeedPlacement(
config: feedConfig,
onFullScreenChange: (bool fullScreen) {
if (!mounted) return;
setState(() => _fullScreen = fullScreen);
SystemChrome.setEnabledSystemUIMode(
fullScreen ? SystemUiMode.immersive : SystemUiMode.edgeToEdge,
);
},
),
],
),
);
}
}
Collapse surrounding content with Visibility(maintainState: true) or a zero height — never by removing it from the tree. Removing a sibling re-parents the WebView and it stops painting entirely.
Dark mode
Rebuild with a new config; the placement pushes the change into the already-loaded page. There is no controller call for this.
final isDark = MediaQuery.platformBrightnessOf(context) == Brightness.dark;
FeedPlacement(config: feedConfig.copyWith(darkMode: isDark));
Best Practices
onUnsupportedonUnsupported is the only route for parity gaps and page-reported errors — the plugin logs nothing of its own for them. Wire it up in any build you intend to debug.
No widget may cover the placement — not even a transparent overlay. It breaks viewability measurement.
Configuration
Every FeedPlacementConfig field and FeedPlacement property is in Settings.
Ad Lifecycle Listener
Feed's onEvent uses the same typed TeadsAdEvent vocabulary as the other placements, and adds onUnsupported, onOrganicClick and onFullScreenChange. See Placement Events Listener.
Checklist
- ✅ Ensure Brand Safety is enabled: pass the real
articleUrl - ✅ Ensure you comply with privacy legal requirements (GDPR/CCPA/GPP)
- ✅ Comply with app-ads.txt
- ✅
widgetIndex: 0unless you deliberately coordinate multiple feeds - ✅ Test with the IDs in Test PIDs and Configurations