# 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](https://www.airship.com/docs/guides/features/messaging/scenes/embedded-content/).

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](https://github.com/urbanairship/android-library/blob/18.0.0/documentation/migration/migration-guide-17-18.md).

## 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 Kotlin


**app build.gradle.kts**


```kotlin
dependencies {
    val airshipVersion = "androidSdkVersion"

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



#### Gradle Groovy


**app build.gradle**


```groovy
dependencies {
    def airshipVersion = "androidSdkVersion"

    // 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**

```kotlin
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**

```kotlin
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**

```kotlin
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](#placing-in-a-scrolling-container) 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**

```kotlin
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](#jetpack-compose-setup).

### 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**

```xml
<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**

```xml
<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**

```kotlin
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.
 

#### Compose



```kotlin
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.



#### XML View


```kotlin
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.


#### Kotlin


**Flow usage**


```kotlin
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")
}
```



#### Java


**Callback usage**


```java
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.
