Skip to main content

Feed Placement

Feed (SmartFeed) is a ready-made, self-paginating content recommendations widget. Drop FeedPlacement into a scrolling list and it handles the rest.


Sample App

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 position

Only 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,
);
},
),
],
),
);
}
}
Do not change the widget structure around the placement

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​

Wire up onUnsupported

onUnsupported 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.

Nothing on top of the placement

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​