Recommendations
Recommendations is data only: the SDK fetches recommendations and renders nothing — you build the list UI. There is no native view and no dispose(); each fetch is an independent request/response round trip.
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
final recommendations = await Recommendations.fetch(
RecommendationsPlacementConfig.url(
widgetId: 'SDK_1', // Provided by your Teads account manager
articleUrl: 'https://example.com/article/123',
),
);
ListView.builder(
itemCount: recommendations.length,
itemBuilder: (context, index) {
final rec = recommendations[index];
return ListTile(
leading: rec.thumbnail?.url != null ? Image.network(rec.thumbnail!.url!) : null,
title: Text(rec.content ?? ''),
subtitle: Text(rec.sourceName ?? ''),
trailing: rec.shouldDisplayDisclosureIcon && rec.disclosure?.iconUrl != null
? GestureDetector(
// launchUrl comes from package:url_launcher.
onTap: () {
final clickUrl = rec.disclosure!.clickUrl;
if (clickUrl != null) launchUrl(Uri.parse(clickUrl));
},
child: Image.network(rec.disclosure!.iconUrl!),
)
: null,
onTap: () => _onRecommendationTap(rec),
);
},
)
Call Recommendations.release(recommendations) once a list is no longer shown, to free the native entries that registerClick resolves against. It is optional, since that cache is also bounded.
Request types
RecommendationsPlacementConfig has two mutually exclusive named constructors, mirroring the native request shapes:
// URL-based
RecommendationsPlacementConfig.url(
widgetId: 'SDK_1',
articleUrl: 'https://example.com/article/123',
externalId: 'optional-external-id',
);
// Platform-based: for an app or portal rather than one article.
// One of contentUrl / portalUrl / bundleUrl must be set, and lang is required
// by the server.
RecommendationsPlacementConfig.platform(
widgetId: 'SDK_1',
portalUrl: 'https://example.com',
lang: 'en',
);
Handling a tap
Future<void> _onRecommendationTap(TeadsRecommendation rec) async {
final url = await Recommendations.registerClick(rec);
if (url == null) return;
// Open `url` however your app opens links — an external browser for a paid item,
// your own reader UI for an organic one (`rec.isPaid` tells you which).
}
registerClick returns null if the item has no click handle, or if the native side no longer has it cached.
registerClick only at the moment of an actual tapregisterClick resolves the destination URL and reports the click — for an organic recommendation it fires the click-tracking request as a side effect. Calling it eagerly (for example right after fetch, to pre-compute URLs) reports a phantom click for every organic item whether the user taps it or not. This is why TeadsRecommendation has no clickUrl field.
AdChoices and disclosure icons — required
Two icons are required to ship a recommendations widget:
- A clickable AdChoices icon above the list, at least 30×30 points, hidden until recommendations load. When tapped, open the URL from
Recommendations.adChoicesUrl. - The per-item disclosure icon on paid items: show
rec.disclosurewhenrec.shouldDisplayDisclosureIconistrue(as in the example above), openingrec.disclosure!.clickUrlwhen tapped.
// launchUrl comes from package:url_launcher.
final adChoicesUrl = await Recommendations.adChoicesUrl;
IconButton(
icon: const Icon(Icons.info_outline),
iconSize: 30,
onPressed: adChoicesUrl == null ? null : () => launchUrl(Uri.parse(adChoicesUrl)),
)
To see paid items (and so the disclosure icons) while testing, enable test mode and testRTB before fetching — testRTB does nothing outside test mode:
await TeadsSdk.setTestMode(true);
await Recommendations.setTestRtb(true);
Both are process-wide — remove them before release.
Available fields
TeadsRecommendation exposes only the fields present, with the same meaning, on both platforms:
| Field | Type | Notes |
|---|---|---|
content | String? | The recommendation's title |
author | String? | |
sourceName | String? | Publisher/source name to display |
position | String? | Position within the fetched set |
publishDate | DateTime? | |
isPaid | bool | true for a paid (3rd-party) item, false for organic |
isVideo | bool | Always false on iOS — that is native's own behavior |
sameSource | bool | |
shouldDisplayDisclosureIcon | bool | Show disclosure when true |
thumbnail | TeadsRecommendationThumbnail? | url, width, height |
disclosure | TeadsRecommendationDisclosure? | iconUrl, clickUrl |
reqId | String? | Needed for viewability reporting |
Android-only extras (description, advertiserName, ctaText, and others) are deliberately dropped rather than sent as null on iOS.
Viewability
Because you render the list, impression reporting is yours to drive too. See Recommendations Viewability — it is required for revenue on recommendation placements.
Configuration
Every RecommendationsPlacementConfig field and Recommendations member is in Settings.
Checklist
- ✅ Ensure Brand Safety is enabled: pass the real
articleUrl - ✅ Ensure you comply with privacy legal requirements (GDPR/CCPA/GPP)
- ✅ AdChoices icon above the list and disclosure icons on paid items
- ✅
registerClickonly on a real tap - ✅ Viewability reporting wired up
- ✅
setTestMode/setTestRtbremoved before release