Android Embedded Content

Embedded Content is an alternative Scene format that can be displayed on any app screen in a view defined by an app developer. It can also be presented in Story format. Android SDK 18.1.4+

About Embedded Content

Present the content of a SceneA single or multi-screen in-app experience cached on users’ devices and displayed when users meet certain conditions in your app or website, such as viewing a particular screen or when a Custom Event occurs. They can be presented in fullscreen, modal, or embedded format using the default swipe/click mode or as a Story. Scenes can also contain survey questions. on any app screen. There are three primary components:

ComponentDescription
A "view" in your app where the content will displayAn app developer creates an AirshipEmbeddedView that controls the dimensions of the content and its location in your app. They also determine what content can be displayed in the view by setting a value for the view's embeddedId that matches the ID of an Embedded Content view style.
A view style in your project settingsA marketer creates an Embedded Content view style and assigns an ID for reference in the app view's embeddedId.
A Scene using an Embedded Content view styleThis is the source of the content that will be displayed in the view.

Once the Scene is triggered for display and matches the specified audience conditions, its content is available to users when visiting a screen that contains the AirshipEmbeddedView. The view is populated with the content from all active Scenes with the matching ID and in the order in which the Scenes were triggered.

Embedded Content behavior is the same as In-App AutomationsMessages cached on users’ devices and displayed when users meet certain conditions within your app, such as viewing a particular screen or opening the app a certain number of times., SurveysA question-and-answer experience used to collect and aggregate feedback or generate a net promoter score. Surveys are cached on users’ devices and displayed when users meet certain conditions within your app, such as viewing a particular screen or opening the app a certain number of times., and modal and fullscreen Scenes:

  • The content displays only within the app.
  • When the app is terminated, the content is not automatically dismissed. It continues to display in the next app session.

You can set up Embedded Content for Android using Jetpack Compose or XML Views. If you are not already on Android SDK 18.1.4+, see the Airship Android SDK 17.x to 18.0 migration guide.

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

app build.gradle.kts

dependencies {
    val airshipVersion = "17.8.0"

    // Other Airship dependencies...
    
    implementation("com.urbanairship.android:urbanairship-automation-compose:$airshipVersion")
}
 Note

All Airship dependencies included in the build.gradle.kts file should all specify the exact same version.

app build.gradle

dependencies {
    def airshipVersion = "17.8.0"

    // Other Airship dependencies...

    implementation "com.urbanairship.android:urbanairship-automation-compose:$airshipVersion"
}
 Note

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.

Basic integration
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, the AirshipEmbeddedView will display it within your composition.
  • If no content is available for the embeddedId, the AirshipEmbeddedView will not be visible.
  • Compose previews will show a default placeholder that displays the embeddedId.

Basic integration with placeholder
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.

verticalScroll modifier example
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.

Lazy scrolling example
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.

Basic integration
<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, the AirshipEmbeddedView will display it within your layout.
  • If no content is available for the airshipEmbeddedId, the AirshipEmbeddedView will not be visible.

Basic integration with placeholder
<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.

ScrollView example
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.

Controlling content display order

By default, pending Embedded Content is displayed in First In, First Out (FIFO) order per embeddedId. If you want to control the order in which pending content is displayed, you can provide a custom Comparator to sort the Embedded Content based on fields that you define in the content’s extras.

Sorting by extras



AirshipEmbeddedView(
    embeddedId = "home_banner",
    comparator = { 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()
)

A Comparator can also be passed as an argument to rememberAirshipEmbeddedViewState to control content display order when hoisting the embedded view state.

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    val embeddedView = findViewById<AirshipEmbeddedView>(R.id.home_banner_embedded_view)

    embeddedView.comparator = Comparator { 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)
    }
}

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

Flow usage

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")
}
Callback usage

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.