Embedded Content
Integrate Embedded Content into your Android app to display Scene content directly within your app’s screens.
For information about Embedded Content, including overview, use cases, and how to create Embedded Content view styles and Scenes, see Embedded Content.
You can set up Embedded Content for Android using Jetpack Compose or XML Views. If you’re upgrading from an earlier SDK version, see the Android SDK migration guides.
Jetpack Compose setup
Embedded Content support for Jetpack Compose is provided by an extension library, which must be declared as a dependency of your project.
Gradle dependencies
dependencies {
val airshipVersion = "21.0.0"
// Other Airship dependencies...
implementation("com.urbanairship.android:urbanairship-automation-compose:$airshipVersion")
}
All Airship dependencies included in the build.gradle.kts file should all specify the exact same version.
dependencies {
def airshipVersion = "21.0.0"
// Other Airship dependencies...
implementation "com.urbanairship.android:urbanairship-automation-compose:$airshipVersion"
}
All Airship dependencies included in the build.gradle file should all specify the exact same version.
Adding an AirshipEmbeddedView
The AirshipEmbeddedView is a Composable UI element that defines a place for Airship Embedded Content to be displayed. When defining an AirshipEmbeddedView, specify the embeddedId for the content it should display. The value of the embeddedId must be the ID of an Embedded Content view style in your project.
import com.urbanairship.automation.compose.AirshipEmbeddedView
@Composable
fun HomeScreenBanner() {
// Show any "home_banner" Embedded Content
AirshipEmbeddedView(
embeddedId = "home_banner",
modifier = Modifier.fillMaxWidth().height(300.dp)
)
}Placeholders
If no content is available to display, the embedded view can optionally show a placeholder. The placeholder can be configured by providing a composable lambda that defines the placeholder content. If no placeholder is set, the embedded view will use the default behavior:
- If content is available for the
embeddedId, theAirshipEmbeddedViewwill display it within your composition. - If no content is available for the
embeddedId, theAirshipEmbeddedViewwill not be visible. - Compose previews will show a default placeholder that displays the
embeddedId.
AirshipEmbeddedView(
embeddedId = "home_banner",
modifier = Modifier.fillMaxWidth().wrapContentHeight()
) {
Text("Placeholder!", Modifier.align(Alignment.Center))
}Placing in a Scrolling Container
When placed directly in a scrolling Composable, or in a nested Composable within the scrolling parent that is not bounded in
the scroll direction, you must provide the parent’s size for the corresponding dimension of the embedded view. This enables
percent-based sizing to work correctly. A simple way to accomplish this is to use the onSizeChanged modifier to store
the size of the scrolling parent (or another ancestor) so that the size can be passed to the embedded view via the
parentWidthProvider or parentHeightProvider arguments.
val scrollState = rememberScrollState()
var parentHeight by remember { mutableIntStateOf(0) }
Column(
modifier = Modifier.fillMaxSize()
.onSizeChanged { parentHeight = it.height }
.verticalScroll(scrollState)
) {
AirshipEmbeddedView(
embeddedId = "home_banner",
parentHeightProvider = { parentHeight },
modifier = Modifier.fillMaxWidth()
)
// ...
}Placing in a Lazy Container
An approach similar to the above method can be used for sizing embedded views inside of Lazy scrolling containers, such as LazyColumn or LazyRow. It’s important to remember to hoist the embedded view state above the Lazy container so that the embedded view can be recycled and re-created properly. You can do this by calling rememberAirshipEmbeddedViewState and passing the embedded view ID as an argument, which returns an embedded view state-holder instance for the given embeddedId.
In the example below, you’ll notice that the AirshipEmbeddedView call doesn’t include an embeddedId argument. This is because the embeddedId is provided by the remembered AirshipEmbeddedViewState instance.
val lazyListState = rememberLazyListState()
// Hoist the embedded state above the LazyColumn.
val embeddedViewState = rememberAirshipEmbeddedViewState(embeddedId = "home_banner")
var parentHeight by remember { mutableIntStateOf(0) }
LazyColumn(
state = lazyListState,
modifier = Modifier.fillMaxSize()
.onSizeChanged { parentHeight = it.height }
) {
item {
AirshipEmbeddedView(
// The embeddedId of "home_banner" from embeddedViewState
// will be used by the embedded view.
state = embeddedViewState,
parentHeightProvider = { parentHeight },
modifier = Modifier.fillMaxWidth()
)
}
// ...
}XML Views setup
You can use XML Views instead of Jetpack Compose.
Adding an AirshipEmbeddedView
The AirshipEmbeddedView is an Android View that defines a place for Airship Embedded Content to be
displayed.
When defining an AirshipEmbeddedView, specify the airshipEmbeddedId for the content it should display. The value of the embeddedId must be the ID of an Embedded Content view style in your project.
<com.urbanairship.embedded.AirshipEmbeddedView
android:id="@+id/home_banner_embedded_view"
android:layout_width="match_parent"
android:layout_height="300dp"
app:airshipEmbeddedId="home_banner" />Placeholders
If no content is available to display, the embedded view can optionally show a placeholder. The placeholder can be configured by providing a reference to an XML layout that defines the placeholder content. If no placeholder is set, the embedded view will use the default behavior:
- If content is available for the
airshipEmbeddedId, theAirshipEmbeddedViewwill display it within your layout. - If no content is available for the
airshipEmbeddedId, theAirshipEmbeddedViewwill not be visible.
<com.urbanairship.embedded.AirshipEmbeddedView
android:id="@+id/home_banner_embedded_view"
android:layout_width="match_parent"
android:layout_height="300dp"
app:airshipEmbeddedId="home_banner"
app:airshipPlaceholder="@layout/include_embedded_placeholder_item" />Placing in a ScrollView or RecyclerView
When placed directly in a ScrollView or RecyclerView, or as a nested child view within a scrolling view that is not bounded in
the scroll direction, you must provide the parent’s size for the corresponding dimension of the embedded view. This enables
percent-based sizing to work correctly. You’ll need to determine the container size of the
scrolling parent (or another ancestor) and pass the size to the embedded view via the
parentWidthProvider or ParentHeightProvider arguments.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
val scrollView = findViewById<ScrollView>(R.id.scroll_view)
val embeddedView = findViewById<AirshipEmbeddedView>(R.id.home_banner_embedded_view)
scrollView.doOnPreDraw {
embeddedView.parentHeightProvider = { scrollView.height }
}
}Use with RecyclerView is similar to the above example, but you’ll need to set the parent size in the onBindViewHolder method.
One way to accomplish this is to pass the parent size to the adapter so that it can be used when binding the view holder
that contains the embedded view.
Selection
When more than one instance of Embedded Content is pending for the same embeddedId, AirshipEmbeddedView uses a selection strategy to decide which one to display. Pass a selection argument to the Composable, or set the selection property on the XML view.
Priority
AirshipEmbeddedSelection.Priority is the default strategy. The pending instance with the lowest numeric priority value is displayed. Once an instance is displayed, it stays displayed for as long as it remains pending, even if a higher-priority instance arrives, so content doesn’t change out from under a user who is already viewing it.
Priority selection
AirshipEmbeddedView(
embeddedId = "home_banner",
selection = AirshipEmbeddedSelection.Priority,
modifier = Modifier.fillMaxWidth()
)embeddedView.selection = AirshipEmbeddedSelection.PriorityComparator
AirshipEmbeddedSelection.ByComparator sorts pending instances with a Comparator you supply, based on fields you define in the content’s extras. The instance that sorts first is displayed. Sticky last-displayed behavior is bypassed.
Comparator selection
AirshipEmbeddedView(
embeddedId = "home_banner",
selection = AirshipEmbeddedSelection.ByComparator { a, b ->
// Compare based on the priority field set on the Embedded Content extras.
val priorityA = a.extras.opt("priority").getInt(0)
val priorityB = b.extras.opt("priority").getInt(0)
priorityA.compareTo(priorityB)
},
modifier = Modifier.fillMaxWidth()
)embeddedView.selection = AirshipEmbeddedSelection.ByComparator { a, b ->
// Compare based on the priority field set on the Embedded Content extras.
val priorityA = a.extras.opt("priority").getInt(0)
val priorityB = b.extras.opt("priority").getInt(0)
priorityA.compareTo(priorityB)
}Instance
AirshipEmbeddedSelection.ByInstanceId takes an ordered list of instance IDs and displays the earliest one that’s pending. Instance IDs are runtime-generated, so obtain them from an AirshipEmbeddedObserver before passing them. The placeholder shows until one of them is pending.
This is an allow-list: pending content not named in the list is excluded entirely rather than ordered after it, so newly pending content won’t display until you add it to the list.
Instance selection
AirshipEmbeddedView(
embeddedId = "home_banner",
selection = AirshipEmbeddedSelection.ByInstanceId(instanceIds),
modifier = Modifier.fillMaxWidth()
)embeddedView.selection = AirshipEmbeddedSelection.ByInstanceId(instanceIds)A single-ID constructor is also available for targeting one instance: AirshipEmbeddedSelection.ByInstanceId(instanceId).
AI selection
AI Android SDK 21+
AirshipEmbeddedSelection.ByAi uses an on-device model to score each pending instance and display the best match. Set up On-Device AI before using it.
AirshipEmbeddedView(
embeddedId = "home_banner",
selection = AirshipEmbeddedSelection.ByAi(
config = AirshipEmbeddedSelection.ByAi.Config(
prompt = "Show content that matches the user's current interests."
)
),
modifier = Modifier.fillMaxWidth()
)The placeholder shows while the model runs. If the model is unavailable, has nothing to tell the candidates apart, or scores below minScoreThreshold, fallback decides instead, defaulting to Priority — an absent model never means an empty view.
By default, once the model chooses an instance it keeps displaying as others come and go, until it’s dismissed. Set allowDisplayInterruptions to true on Config to let a change in the pending set re-run the model and swap the displayed instance.
Filtering
Android SDK 21+filterInstances decides which pending instances are eligible to be displayed at all; selection decides which of the eligible ones is shown. It’s applied first, so an excluded instance is never displayed even when selection names it, never becomes a page in a group or carousel, and is never offered to an AI selection as a candidate. Prefer it over ByInstanceId when the intent is to exclude specific instances rather than enumerate the acceptable ones.
Filtering instances
AirshipEmbeddedView(
embeddedId = "home_banner",
selection = AirshipEmbeddedSelection.Priority,
filterInstances = { it.instanceId !in suppressedInstanceIds },
modifier = Modifier.fillMaxWidth()
)embeddedView.filterInstances = { it.instanceId !in suppressedInstanceIds }Observing available Embedded Content
Embedded Content is not always available, and even after being triggered, it still needs to be prepared before it can be displayed. An AirshipEmbeddedView will automatically update when content is available and transition from the placeholder to the content once content is available. If you need to query the availability of Embedded Content, you can use an AirshipEmbeddedObserver to watch for updates.
An AirshipEmbeddedObserver exposes both a callback and a Flow that can be used to receive updates about the
availability of Embedded Content. This allows for more dynamic handling of Embedded Content than just content
or a placeholder.
Observer example
val observer = AirshipEmbeddedObserver("playground")
val embeddedInfo = observer.embeddedViewInfoFlow.collectAsState(initial = emptyList())
if (embeddedInfo.value.isEmpty()) {
Text("No banner available")
} else {
Text("Banner available")
AirshipEmbeddedView(embeddedId = "home_banner")
}
AirshipEmbeddedObserver observer = new AirshipEmbeddedObserver("home_banner");
observer.setListener(new AirshipEmbeddedObserver.Listener() {
@Override
public void onEmbeddedViewInfoUpdate(@NonNull List<AirshipEmbeddedInfo> views) {
if (views.isEmpty()) {
textView.setText("No banner available");
embeddedView.setVisibility(View.GONE);
} else {
textView.setText("Banner available");
embeddedView.setVisibility(View.VISIBLE);
}
}
});
The AirshipEmbeddedObserver can be created to watch for one or more embeddedId values or use custom
filtering to watch all Embedded Content or a subset determined by inspecting the embeddedId or
extras associated with the Embedded Content. The embeddedInfos returned by the callback or Flow
are in FIFO order, meaning that the first content in the list is the first content that will be displayed.