Feed Placement
Feed Placement (Content Recommendations)
Feed placements display content recommendation widgets, perfect for keeping users engaged with related content.
Use an Activity context to create TeadsAdPlacementFeed (not the application context). The SDK uses it to open paid recommendation clicks and to size the creative correctly. Passing the application context can cause paid clicks to not open and the ad to be mis-sized.
- Kotlin
- Java
import tv.teads.sdk.combinedsdk.adplacement.TeadsAdPlacementFeed
import tv.teads.sdk.combinedsdk.adplacement.config.TeadsAdPlacementFeedConfig
import tv.teads.sdk.combinedsdk.adplacement.interfaces.TeadsAdPlacementEventsDelegate
import tv.teads.sdk.combinedsdk.adplacement.interfaces.core.TeadsAdPlacement
import tv.teads.sdk.combinedsdk.TeadsAdPlacementEventName
import android.net.Uri
class ContentActivity : AppCompatActivity(), TeadsAdPlacementEventsDelegate {
private var feedPlacement: TeadsAdPlacementFeed? = null
private lateinit var binding: ActivityContentBinding
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityContentBinding.inflate(layoutInflater)
setContentView(binding.root)
setupFeedPlacement()
}
private fun setupFeedPlacement() {
// Create configuration
val config = TeadsAdPlacementFeedConfig(
articleUrl = Uri.parse("https://yoursite.com/article"),
widgetId = "MB_1", // Your unique Placement ID
installationKey = "YOUR_INSTALLATION_KEY",
widgetIndex = 0,
userId = null, // Optional user ID for personalization
darkMode = false,
testDisplay = false,
extId = null, // External ID
extSecondaryId = null, // External Secondary ID
obPubImpl = null // OB Publisher Implementation
)
// Create placement
feedPlacement = TeadsAdPlacementFeed(
this, // Context — MUST be an Activity
config, // Placement config
this // Event delegate
)
// Load the feed
val feedView = feedPlacement?.loadAd()
// Add to your view hierarchy
binding.myContainerAdView.addView(feedView)
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
feedPlacement?.onActivityConfigurationChanged()
}
override fun onPlacementEvent(
placement: TeadsAdPlacement<*, *>,
event: TeadsAdPlacementEventName,
data: Map<String, Any>?
) {
// Listen the ad lifecycle events
if (placement is TeadsAdPlacementFeed && event == TeadsAdPlacementEventName.CLICKED_ORGANIC) {
val url = data?.get("url") as? String
url?.let {
// Programmatically open browser with the url
}
}
}
}
import tv.teads.sdk.combinedsdk.adplacement.TeadsAdPlacementFeed;
import tv.teads.sdk.combinedsdk.adplacement.config.TeadsAdPlacementFeedConfig;
import tv.teads.sdk.combinedsdk.adplacement.interfaces.TeadsAdPlacementEventsDelegate;
import tv.teads.sdk.combinedsdk.adplacement.interfaces.core.TeadsAdPlacement;
import tv.teads.sdk.combinedsdk.TeadsAdPlacementEventName;
import android.content.res.Configuration;
import android.net.Uri;
import android.widget.FrameLayout;
import java.util.Map;
public class ContentActivity extends AppCompatActivity implements TeadsAdPlacementEventsDelegate {
private TeadsAdPlacementFeed feedPlacement;
private ActivityContentBinding binding;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = ActivityContentBinding.inflate(getLayoutInflater());
setContentView(binding.getRoot());
setupFeedPlacement();
}
private void setupFeedPlacement() {
// Create configuration — this 10-arg constructor (no floorPrice) is a real
// Java-visible overload generated for the config's trailing default parameters
TeadsAdPlacementFeedConfig config = new TeadsAdPlacementFeedConfig(
Uri.parse("https://yoursite.com/article"),
"MB_1", // Your unique Placement ID
"YOUR_INSTALLATION_KEY",
0,
null, // userId — Optional user ID for personalization
false, // darkMode
false, // testDisplay
null, // extId — External ID
null, // extSecondaryId — External Secondary ID
null // obPubImpl — OB Publisher Implementation
);
// Create placement
feedPlacement = new TeadsAdPlacementFeed(
this, // Context — MUST be an Activity
config, // Placement config
this // Event delegate
);
// Load the feed
FrameLayout feedView = feedPlacement.loadAd();
// Add to your view hierarchy
binding.myContainerAdView.addView(feedView);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (feedPlacement != null) {
feedPlacement.onActivityConfigurationChanged();
}
}
@Override
public void onPlacementEvent(TeadsAdPlacement<?, ?> placement, TeadsAdPlacementEventName event, Map<String, ?> data) {
// Listen the ad lifecycle events
if (placement instanceof TeadsAdPlacementFeed && event == TeadsAdPlacementEventName.CLICKED_ORGANIC) {
Object url = data != null ? data.get("url") : null;
if (url instanceof String) {
// Programmatically open browser with the url
}
}
}
}
Explore More Feature
The Feed placement supports an "Explore More" feature that shows additional content when users navigate away:
- Kotlin
- Java
// Enable explore more when the user leaves the article
override fun onBackPressed() {
super.onBackPressed()
TeadsAdPlacementFeed.handleExploreMore(this@ContentActivity) {
runOnUiThread { finish() }
}
}
// Enable explore more when the user leaves the article
@Override
public void onBackPressed() {
super.onBackPressed();
// handleExploreMore is a plain static method — no .INSTANCE needed
TeadsAdPlacementFeed.handleExploreMore(this, () -> runOnUiThread(this::finish));
}
Multiple Feed Widgets
You can display multiple feed widgets on the same page by using the widgetIndex parameter. Each widget is identified by its index, starting from 0. To create a second widget, use .copy(widgetIndex = 1) on an existing configuration:
- Kotlin
- Java
// First widget (widgetIndex = 0, the default)
val config = TeadsAdPlacementFeedConfig(
widgetId = "MB_1",
articleUrl = Uri.parse("https://yoursite.com/article"),
installationKey = "YOUR_INSTALLATION_KEY",
widgetIndex = 0
)
// Second widget — reuse the same config, just change widgetIndex
val config2 = config.copy(widgetIndex = 1)
// First widget (widgetIndex = 0, the default) — this 4-arg constructor is a real
// Java-visible overload generated for the config's trailing default parameters
TeadsAdPlacementFeedConfig config = new TeadsAdPlacementFeedConfig(
Uri.parse("https://yoursite.com/article"),
"MB_1",
"YOUR_INSTALLATION_KEY",
0
);
// Second widget — reuse the same config, just change widgetIndex.
// copy() only has ONE Java-visible overload (all 11 fields) — Kotlin's named-arg
// defaulting doesn't apply to it, so every current value must be passed back explicitly
TeadsAdPlacementFeedConfig config2 = config.copy(
config.getArticleUrl(),
config.getWidgetId(),
config.getInstallationKey(),
1,
config.getUserId(),
config.getDarkMode(),
config.getTestDisplay(),
config.getExtId(),
config.getExtSecondaryId(),
config.getObPubImpl(),
config.getFloorPrice()
);
RecyclerView / LazyColumn require sequential loading. In recycling views, the second widget's loadAd() must be deferred until the first widget (widgetIndex = 0) fires the LOADED event. Because RecyclerView and LazyColumn recycle view holders, the second widget may fail to initialize if it loads before the first one completes.
In non-recycling views (ScrollView, Column) both widgets can call loadAd() simultaneously because every view stays attached to the hierarchy.
Below is a simplified RecyclerView example based on the demo app:
- Kotlin
- Java
class Feed2WidgetsFragment : Fragment(), TeadsAdPlacementEventsDelegate {
private lateinit var feedAd: TeadsAdPlacementFeed
private lateinit var feedAd2: TeadsAdPlacementFeed
private lateinit var adapter: ArticleAdapter
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
val config = TeadsAdPlacementFeedConfig(
widgetId = "MB_1",
articleUrl = Uri.parse("https://yoursite.com/article"),
installationKey = "YOUR_INSTALLATION_KEY",
widgetIndex = 0
)
val config2 = config.copy(widgetIndex = 1)
// requireContext() returns the Activity context — required (do not pass applicationContext)
feedAd = TeadsAdPlacementFeed(requireContext(), config, this)
feedAd2 = TeadsAdPlacementFeed(requireContext(), config2, this)
adapter = ArticleAdapter(feedAd, feedAd2)
view.findViewById<RecyclerView>(R.id.recycler_view).apply {
layoutManager = LinearLayoutManager(requireContext())
adapter = this@Feed2WidgetsFragment.adapter
}
}
override fun onPlacementEvent(
placement: TeadsAdPlacement<*, *>,
event: TeadsAdPlacementEventName,
data: Map<String, Any>?
) {
// When the first widget is loaded, allow the second widget to load
if (placement === feedAd && event == TeadsAdPlacementEventName.LOADED) {
adapter.isFirstAdReady = true
view?.post {
adapter.notifyItemChanged(ArticleAdapter.SECOND_AD_POSITION)
}
}
if (event == TeadsAdPlacementEventName.CLICKED_ORGANIC) {
val url = data?.get("url") as? String
// Open URL in browser
}
}
}
class ArticleAdapter(
private val feedAd: TeadsAdPlacementFeed,
private val feedAd2: TeadsAdPlacementFeed
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
var isFirstAdReady = false
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
when (position) {
FIRST_AD_POSITION -> {
val adHolder = holder as TeadsPlacementViewHolder
if (adHolder.container.isEmpty()) {
adHolder.container.addView(feedAd.loadAd())
}
}
SECOND_AD_POSITION -> {
val adHolder = holder as TeadsPlacementViewHolder
// Only load once the first widget has fired LOADED
if (isFirstAdReady && adHolder.container.isEmpty()) {
adHolder.container.addView(feedAd2.loadAd())
}
}
// ... other view types
}
}
companion object {
private const val FIRST_AD_POSITION = 6
const val SECOND_AD_POSITION = 11
}
}
public class Feed2WidgetsFragment extends Fragment implements TeadsAdPlacementEventsDelegate {
private TeadsAdPlacementFeed feedAd;
private TeadsAdPlacementFeed feedAd2;
private ArticleAdapter adapter;
@Override
public void onViewCreated(View view, Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
// 4-arg constructor is a real Java-visible overload (see above)
TeadsAdPlacementFeedConfig config = new TeadsAdPlacementFeedConfig(
Uri.parse("https://yoursite.com/article"),
"MB_1",
"YOUR_INSTALLATION_KEY",
0
);
// copy() needs every field passed back explicitly — see above
TeadsAdPlacementFeedConfig config2 = config.copy(
config.getArticleUrl(),
config.getWidgetId(),
config.getInstallationKey(),
1,
config.getUserId(),
config.getDarkMode(),
config.getTestDisplay(),
config.getExtId(),
config.getExtSecondaryId(),
config.getObPubImpl(),
config.getFloorPrice()
);
// requireContext() returns the Activity context — required (do not pass applicationContext)
feedAd = new TeadsAdPlacementFeed(requireContext(), config, this);
feedAd2 = new TeadsAdPlacementFeed(requireContext(), config2, this);
adapter = new ArticleAdapter(feedAd, feedAd2);
RecyclerView recyclerView = view.findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
recyclerView.setAdapter(adapter);
}
@Override
public void onPlacementEvent(TeadsAdPlacement<?, ?> placement, TeadsAdPlacementEventName event, Map<String, ?> data) {
// When the first widget is loaded, allow the second widget to load
if (placement == feedAd && event == TeadsAdPlacementEventName.LOADED) {
adapter.isFirstAdReady = true;
if (getView() != null) {
getView().post(() -> adapter.notifyItemChanged(ArticleAdapter.SECOND_AD_POSITION));
}
}
if (event == TeadsAdPlacementEventName.CLICKED_ORGANIC) {
Object url = data != null ? data.get("url") : null;
// Open URL in browser
}
}
}
class ArticleAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private static final int FIRST_AD_POSITION = 6;
public static final int SECOND_AD_POSITION = 11;
private final TeadsAdPlacementFeed feedAd;
private final TeadsAdPlacementFeed feedAd2;
public boolean isFirstAdReady = false;
public ArticleAdapter(TeadsAdPlacementFeed feedAd, TeadsAdPlacementFeed feedAd2) {
this.feedAd = feedAd;
this.feedAd2 = feedAd2;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
switch (position) {
case FIRST_AD_POSITION: {
TeadsPlacementViewHolder adHolder = (TeadsPlacementViewHolder) holder;
if (adHolder.container.getChildCount() == 0) {
adHolder.container.addView(feedAd.loadAd());
}
break;
}
case SECOND_AD_POSITION: {
TeadsPlacementViewHolder adHolder = (TeadsPlacementViewHolder) holder;
// Only load once the first widget has fired LOADED
if (isFirstAdReady && adHolder.container.getChildCount() == 0) {
adHolder.container.addView(feedAd2.loadAd());
}
break;
}
// ... other view types
}
}
}