> For the complete documentation index, see [llms.txt](https://docs.firework.com/firework-for-developers/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.firework.com/firework-for-developers/android-sdk/integration-guide/shoppable-videos/product-hydration.md).

# Product Hydration

## Introduction

Product hydration lets you update product information at runtime before it is displayed to the user. This is useful for syncing prices, availability, descriptions, and variants with your own backend.

The Firework SDK supports two hydration modes:

| Mode            | Trigger                                           | Scope                                           | Listener                            |
| --------------- | ------------------------------------------------- | ----------------------------------------------- | ----------------------------------- |
| **Video-level** | A video or ivestream with products starts playing | Products from a single video/livestream         | `setOnProductHydrationListener`     |
| **Feed-level**  | A page of feed data is loaded                     | Products aggregated from all videos in the page | `setOnFeedProductHydrationListener` |

Both modes use the same `ProductHydrator` API to modify products. The sections below explain each mode and the full hydration API.

***

## Hydration Level

### Feed-Level Product Hydration

> **Note**: Feed-level hydration currently only supports **short videos**. Livestream content is not supported.

Feed-level hydration batches all products from a loaded page of feed data into a **single callback**, instead of issuing one callback per video. Products are aggregated across all videos in the page and deduplicated by product ID.

This is the recommended approach for feed views (e.g. `FwPlayerDeckView`) where multiple videos are loaded at once, as it reduces the number of hydration requests from N (one per video) to 1 (one per page).

#### Setup

Register the listener via `FireworkSdk.shopping.setOnFeedProductHydrationListener`:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        FireworkSdk.shopping.setOnFeedProductHydrationListener { products, hydrator ->
            // Fetch latest product data from your backend
            val updatedProducts = myApi.fetchProducts(products.mapNotNull { it.id })

            for (product in products) {
                val productId = product.id ?: continue
                val updated = updatedProducts[productId] ?: continue
                hydrator.hydrate(productId) {
                    name(updated.name)
                    description(updated.description)
                    isAvailable(updated.inStock)
                    for (unit in product.units) {
                        val unitId = unit.id ?: continue
                        variant(unitId) {
                            price(updated.variants[unitId]?.price ?: 0.0)
                        }
                    }
                    this
                }
            }
            hydrator.completeHydration()
        }
    }

    override fun onDestroy() {
        FireworkSdk.shopping.setOnFeedProductHydrationListener(null)
        super.onDestroy()
    }
}
```

{% endtab %}
{% endtabs %}

#### Callback Parameters

| Parameter  | Type              | Description                                                               |
| ---------- | ----------------- | ------------------------------------------------------------------------- |
| `products` | `List<Product>`   | Aggregated and deduplicated products from all videos in the loaded page.  |
| `hydrator` | `ProductHydrator` | Builder used to modify product fields. Same API as video-level hydration. |

Unlike video-level hydration, the callback does **not** include a `videoInfo` parameter because the products span multiple videos.

:exclamation: You **must** call `hydrator.completeHydration()` when finished. Without this call, updated values will not be applied.

#### Behavior

* **Applies to all view types**: Feed-level hydration is a global listener. Once set, it takes effect for all SDK view components (`FwPlayerDeckView`, `FwVideoFeedView`, `FwStoryBlockView`).
* **Automatic per-item disabling (DeckView inline only)**: When a feed-level listener is set, per-item hydration is automatically disabled **only** for `FwPlayerDeckView` inline cards. It does **not** disable per-item hydration for fullscreen playback (whether launched from `FwPlayerDeckView` or `FwVideoFeedView`) or `FwStoryBlockView`.
* **Coexistence**: Setting both `setOnFeedProductHydrationListener` and `setOnProductHydrationListener` at the same time is technically supported. If both are set, feed-level handles DeckView inline hydration while video-level handles fullscreen and StoryBlock. However, this is **not recommended** — see Best Practices below.
* **Timeout**: The SDK waits up to 10 seconds for `completeHydration()` to be called. If the timeout is exceeded or an exception occurs, the original (non-hydrated) products are displayed.
* **Pagination**: Each page load triggers a separate hydration callback. Products that were already hydrated in a previous page are not requested again.

***

### Video-Level Product Hydration

Video-level hydration is triggered each time a video with associated products starts playing. The SDK calls your listener with the products for that specific video.

Register the listener via `FireworkSdk.shopping.setOnProductHydrationListener`:

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        ...
        FireworkSdk.shopping.setOnProductHydrationListener { products, hydrator, videoInfo ->
            for (product in products) {
                product.id?.let { productId ->
                    hydrator.hydrate(productId) {
                        name("Modified product name")
                        description("Modified product description.")
                        isAvailable(false)
                    }
                }
            }
            hydrator.completeHydration()
        }
    }

    override fun onDestroy() {
        FireworkSdk.shopping.setOnProductHydrationListener(null)
        super.onDestroy()
    }
}
```

{% endtab %}
{% endtabs %}

#### Callback Parameters

| Parameter   | Type              | Description                                               |
| ----------- | ----------------- | --------------------------------------------------------- |
| `products`  | `List<Product>`   | Products associated with the video that is about to play. |
| `hydrator`  | `ProductHydrator` | Builder used to modify product fields.                    |
| `videoInfo` | `VideoInfo`       | Information about the video that requested hydration.     |

:exclamation: You **must** call `hydrator.completeHydration()` when finished. Without this call, updated values will not be applied.

:exclamation: All code examples below only show the `hydrate {}` block for brevity. Refer to the example above for the full listener setup pattern. Remember to always call `hydrator.completeHydration()` after all hydrations are done.

### Best Practices

For the simplest and most predictable integration, set **only one** hydration listener — either feed-level or video-level — rather than both.

| Recommendation                                            | When to use                                                                                               |
| --------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| **Feed-level only** (`setOnFeedProductHydrationListener`) | Your app uses `FwPlayerDeckView` and you want to reduce hydration requests from N-per-page to 1-per-page. |
| **Video-level only** (`setOnProductHydrationListener`)    | You need per-video `videoInfo` in the callback, or your app primarily uses video and also livestream.     |

Setting a single listener avoids confusion about which listener handles which context, simplifies debugging, and ensures consistent hydration logic across all surfaces.

***

## Product Hydration

Use `ProductBuilder` methods inside the `hydrate {}` block to modify product-level properties. These methods apply to both video-level and feed-level hydration.

#### Basic Attributes

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    name("Updated Product Name")
    description("Updated product description.")
    subtitle("Limited Edition")
    currency("USD")
    isAvailable(true)
}
```

{% endtab %}
{% endtabs %}

| Method                              | Description                                                          |
| ----------------------------------- | -------------------------------------------------------------------- |
| `name(name: String)`                | Sets or updates the product name.                                    |
| `description(description: String)`  | Sets or updates the product description.                             |
| `subtitle(subtitle: String?)`       | Sets or updates the product subtitle. Pass `null` to clear.          |
| `currency(currency: String)`        | Sets the product-level currency code string (e.g. `"USD"`, `"EUR"`). |
| `isAvailable(isAvailable: Boolean)` | Sets the availability status of the product.                         |

#### Main Product Image

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    mainProductImage("https://example.com/images/product-hero.jpg")
}
```

{% endtab %}
{% endtabs %}

| Method                          | Description                                                                                                                       |
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `mainProductImage(url: String)` | Sets or updates the main product image URL. If a main image already exists, updates its URL; otherwise creates a new image entry. |

#### Display Control

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    isDisabled(false)
    hidePrice(true)
    hidden(null)
}
```

{% endtab %}
{% endtabs %}

| Method                            | Description                                                                                  |
| --------------------------------- | -------------------------------------------------------------------------------------------- |
| `isDisabled(isDisabled: Boolean)` | Sets the disabled status of the product. A disabled product is treated as unavailable.       |
| `hidePrice(hidePrice: Boolean)`   | Controls whether the product price is hidden from display.                                   |
| `hidden(hidden: Boolean?)`        | Sets whether the product should be hidden entirely. Pass `null` to use the default behavior. |

#### CTA (Call-To-Action) Customization

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    customCTATitle("Buy Now")
    customCTATitleTranslation("Comprar Ahora")
    customCTAUrl("https://example.com/checkout/product-123")
    customCTATarget("_blank")
    hidePrimaryCTA(false)
}
```

{% endtab %}
{% endtabs %}

| Method                                      | Description                                                                        |
| ------------------------------------------- | ---------------------------------------------------------------------------------- |
| `customCTATitle(title: String?)`            | Sets a custom CTA button title. Pass `null` to clear.                              |
| `customCTATitleTranslation(title: String?)` | Sets a translated CTA title for localization. Pass `null` to clear.                |
| `customCTAUrl(url: String?)`                | Sets the URL the CTA button links to. Pass `null` to clear.                        |
| `customCTATarget(target: String?)`          | Sets the CTA target identifier. Pass `null` to clear.                              |
| `hidePrimaryCTA(hide: Boolean?)`            | Controls whether the primary CTA button is hidden. Pass `null` to use the default. |

***

## Backend-selected variants

Since **6.35.1**, a product can represent a specific variant selected by the backend. When several variants are selected, the SDK can render a separate display item for each, with its own image, price, availability, and initial product-detail selection.

| Property                        | Meaning                                                                                          |
| ------------------------------- | ------------------------------------------------------------------------------------------------ |
| `Product.selectedProductUnitId` | SDK identifier for the selected variant; not a commerce SKU. `null` means no specific selection. |
| `Product.selectedUnit`          | Resolved selected variant; `null` if no selection exists or no variant matches.                  |
| `Product.selectedProductSKU`    | Selected variant's commerce ID (`selectedUnit?.id`); `null` if unavailable.                      |

Use `selectedProductSKU` or `selectedUnit?.id` when matching your commerce variants. Do not use `selectedProductUnitId` as a commerce SKU.

Feed-level hydration still receives one entry per product ID, even when that product produces multiple variant cards. The callback entry has no per-card `selectedProductUnitId`. Hydrate the whole product and its variants; the SDK applies the result to each display item while preserving its selection.

### Updating selected variants

When replacing variants, reuse the same `ProductUnit.id` values used by your commerce system. This keeps selections, stock updates, and offers associated with the correct variants.

A backend-selected variant needed by a display item is retained even if it is omitted from `replaceVariants(...)`. This protection also applies when the feed-level callback product has no selected ID of its own. Use `variant(id)` to update an existing variant's fields rather than deleting and recreating it under a different commerce ID.

## Variant Management

### Add a new variant

Appends a new variant to the existing list of product variants. Image management is handled automatically.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    addVariant(
        ProductUnit(
            id = "variant_blue_l",
            name = "Blue / L",
            price = Money(amount = 19.0, CurrencyCode.USD),
            originalPrice = Money(amount = 29.0, CurrencyCode.USD),
            image = ProductImage("https://example.com/images/blue-l.jpg"),
            url = "https://example.com/products/variant_blue_l",
            options = listOf(
                ProductUnitOption(name = "color", value = "Blue"),
                ProductUnitOption(name = "size", value = "L"),
            ),
        ),
    )
}
```

{% endtab %}
{% endtabs %}

### Remove a variant

`removeVariant(id)` is **deprecated since 6.35.1**. It does nothing when the ID belongs to a backend-selected variant needed for display. For other variants it removes the variant and its associated image.

Migrate to `replaceVariants(variantsToKeep)` using the variants you want to retain. Backend-selected variants remain protected even if omitted. To update a selected variant, use [`variant(id)`](#modify-existing-variants).

### Clear all existing variants

`clearVariants()` is **deprecated since 6.35.1**. It retains backend-selected variants and their images, so the resulting list is not necessarily empty.

Migrate from `clearVariants()` followed by `addVariant(...)` to a single `replaceVariants(...)` call. Reuse the existing commerce variant IDs. See the replacement example below.

### Replace existing variants with new variants

Updates the variant list in one call. Use the existing commerce IDs for variants that you are updating. Backend-selected variants needed for display are retained, unchanged, if omitted from the replacement list. The final list may therefore contain more variants than the list you supply.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
val productId = product.id
if (productId != null) {
    hydrator.hydrate(productId) {
        allowedVariantOptions(listOf("color", "size"))
        val variant = ProductUnit(
            id = "variant_blue_l",
            name = "Blue / L",
            price = Money(amount = 19.0, CurrencyCode.USD),
            originalPrice = Money(amount = 29.0, CurrencyCode.USD),
            image = ProductImage("https://example.com/images/blue-l.jpg"),
            url = "https://example.com/products/variant_blue_l",
            options = listOf(
                ProductUnitOption(name = "color", value = "Blue"),
                ProductUnitOption(name = "size", value = "L"),
            ),
        )
        replaceVariants(listOf(variant))
    }
}
```

{% endtab %}
{% endtabs %}

### Modify existing variants

Locates a variant by its unique identifier and applies modifications through a `ProductVariantBuilder` block. If no variant with the given ID is found, the call is a no-op.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    product.units.forEach { unit ->
        unit.id?.let { variantId ->
            variant(variantId) {
                name("Updated Variant Name")
                price(30.0)
                originalPrice(40.0)
                currency(CurrencyCode.USD)
                url("https://example.com/products/updated-variant")
                imageUrl("https://example.com/images/updated.jpg")
                isAvailable(true)
                unitOptions(
                    listOf(
                        ProductUnitOption(name = "color", value = "Blue"),
                        ProductUnitOption(name = "size", value = "L"),
                    ),
                )
            }
        }
    }
    this
}
```

{% endtab %}
{% endtabs %}

***

## Important Notes

#### `allowedVariantOptions`

Sets the list of attribute names that are permissible for product variants. This ensures that all product variants conform to the expected attributes such as color, size, material, etc.

:exclamation: Call this method **before** `replaceVariants()` or adding new variants, so their option names match the configured attributes.

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
hydrator.hydrate(product.id) {
    allowedVariantOptions(listOf("color", "size"))
    // Now add variants that have "color" and "size" options
    addVariant(...)
}
```

{% endtab %}
{% endtabs %}

#### `completeHydration()`

Must be called after all `hydrate()` calls are finished. Without this call, the SDK will not apply any hydrated values.

***

## API Reference

{% tabs %}
{% tab title="Kotlin" %}

```kotlin
interface ProductHydrator {

    fun hydrate(
        productId: String,
        builderBlock: ProductBuilder.() -> ProductBuilder,
    )

    fun completeHydration(): List<Product>

    class ProductBuilder {
        // -- Basic Attributes --
        fun name(name: String): ProductBuilder
        fun description(description: String): ProductBuilder
        fun subtitle(subtitle: String?): ProductBuilder
        fun currency(currency: String): ProductBuilder
        fun isAvailable(isAvailable: Boolean): ProductBuilder

        // -- Main Product Image --
        fun mainProductImage(url: String): ProductBuilder

        // -- Display Control --
        fun isDisabled(isDisabled: Boolean): ProductBuilder
        fun hidePrice(hidePrice: Boolean): ProductBuilder
        fun hidden(hidden: Boolean?): ProductBuilder

        // -- CTA Customization --
        fun customCTATitle(title: String?): ProductBuilder
        fun customCTATitleTranslation(title: String?): ProductBuilder
        fun hidePrimaryCTA(hide: Boolean?): ProductBuilder
        fun customCTATarget(target: String?): ProductBuilder
        fun customCTAUrl(url: String?): ProductBuilder

        // -- Variant Management --
        fun addVariant(variant: ProductUnit): ProductBuilder
        // Deprecated since 6.35.1: use replaceVariants(); selected variants are protected.
        fun removeVariant(id: String): ProductBuilder
        // Deprecated since 6.35.1: selected variants and their images are retained.
        fun clearVariants(): ProductBuilder
        fun replaceVariants(variants: List<ProductUnit>): ProductBuilder
        fun allowedVariantOptions(options: List<String>): ProductBuilder
        fun variant(
            id: String,
            builderBlock: ProductVariantBuilder.() -> ProductVariantBuilder,
        ): ProductBuilder
    }

    class ProductVariantBuilder {
        fun name(name: String): ProductVariantBuilder
        fun price(price: Double): ProductVariantBuilder
        fun originalPrice(price: Double): ProductVariantBuilder
        fun currency(currencyCode: CurrencyCode): ProductVariantBuilder
        fun url(url: String): ProductVariantBuilder
        fun imageUrl(url: String): ProductVariantBuilder
        fun isAvailable(isAvailable: Boolean): ProductVariantBuilder
        fun unitOptions(options: List<ProductUnitOption>): ProductVariantBuilder
    }
}
```

{% endtab %}
{% endtabs %}
