---
agentTools:
  projectIndex: https://docs.storyly.io/llms.txt
---

# Initial Setup for Jetpack Compose

This guide covers the core concepts, architectural benefits, and a step-by-step implementation to help you integrate Storyly Placement into your Jetpack Compose application.

## What is Placement

Storyly Placement is a powerful, server-driven framework designed to dynamically render various widget experiences—such as Story Bars, Banners, and Swipe Cards—within a single host view. By decoupling the UI configuration from your app's codebase, Placement enables you to switch between different widget types in real-time without requiring a new app release.

> 📘 Placement Architecture
>
> ### Server Driven Surface
>
> Placement is a server-driven surface that can render different Storyly experiences (e.g., Story Bar, Banner) in a single host view, based on configuration and rules fetched at runtime.
>
> ### Single Integration Point
>
> It centralizes data loading, rendering, analytics, and commerce hooks via a single provider and a single composable, enabling sophisticated, dynamic experiences without hardcoding which widget to show.

## Core Building Blocks

These core building blocks are mandatory to integrate and start enabling features with Storyly Flows

> 📘 Placement Blocks
>
> ### Placement Data Provider
>
> It fetches and manages the content/config for a placement and exposes listener callbacks. It is caller-owned and must outlive recomposition.
>
> ### Placement Config
>
> It controls test mode, user/targeting context, and layout direction.
>
> ### StorylyPlacement Composable
>
> It hosts the actual widget determined at runtime (e.g., Story Bar, Banner), exposes UI and analytics callbacks, and is sized by the `Modifier` you give it.

## Setup SDK

### Import Module

Before you use Storyly Placement in your app, you must first import the Storyly Placement Compose module.

You need to declare the dependency on module (build.gradle)

```kotlin Kotlin
implementation("com.appsamurai.storyly:storyly-placement-compose:<latest-version>")
```

> 📘 Tip
>
> Please do not forget to replace `<latest-version>`. The latest version on [Maven Central](https://central.sonatype.com/artifact/com.appsamurai.storyly/storyly-placement-compose/overview)
>
> You can find the underlying Storyly Placement SDK's release notes [here](https://github.com/Netvent/storyly-sdk/blob/master/android/RELEASENOTES.md).

> 📘 Info
>
> `storyly-placement-compose` re-exports `storyly-placement`, so you do not need to declare it separately. All callback and payload types (`STRListener`, `STRWidgetController`, `STRPayload`, …) come along transitively.

> 🚧 Warning
>
> Storyly SDK targets `Android API level 24 (Android 7.0, Nougat)` or higher.

> 🚧 Warning
>
> If your application targets devices that does not contain Google APIs, you need to initialize [EmojiCompat](https://developer.android.com/reference/androidx/emoji/text/EmojiCompat) class to use Emoji related features of Storyly such as Emoji and Rating Components. Otherwise, you will encounter a crash whenever you use any of these components in your Storyly instance.
>
> Please follow [Emoji Compat Bundled Fonts initialization steps](https://developer.android.com/develop/ui/views/text-and-emoji/emoji-compat?hl=tr#bundled-fonts) to use Emoji features of Storyly.

## Initialize Components

This section explains how to set up and connect the core Placement components: data provider, placement composable, and listener callbacks.

### STRPlacementDataProvider

The data provider is caller-owned and must **survive recomposition**. Creating it inside a composable body without remembering it would rebuild the provider — and reload the placement — on every recomposition.

A `ViewModel` is the recommended home: it keeps the provider alive across configuration changes, and scopes it to the screen that uses it.

```kotlin Kotlin
class PlacementViewModel(application: Application) : AndroidViewModel(application) {

    val provider: STRPlacementDataProvider =
        STRPlacementDataProvider(application).apply {
            config = STRPlacementConfig.Builder()
                .build(token = "<your_placement_token>")
        }
}
```

> 🚧 Warning
>
> A provider's token is fixed when its config is built. If you need to show a different placement token, you must create a new `STRPlacementDataProvider` instead of rebuilding the config on the existing one.

If you do not use a `ViewModel`, you must at least `remember` the provider so it is created once per composition:

```kotlin Kotlin
val context = LocalContext.current
val provider = remember {
    STRPlacementDataProvider(context.applicationContext).apply {
        config = STRPlacementConfig.Builder()
            .build(token = "<your_placement_token>")
    }
}
```

### STRPlacementConfig

You configure a placement by supplying a `STRPlacementConfig`, which specifies the placement token and defines how the data provider should operate. The config does not load or render anything on its own; instead, it provides the essential metadata the data provider needs to retrieve and manage placement content.

```kotlin Kotlin
config = STRPlacementConfig.Builder()
    .build(token = "<your_placement_token>")
```

Build the config where you build the provider, so the token is set before the first load — see the `PlacementViewModel` example above.

> 🚧 Warning
>
> Please login to Storyly dashboard and get your placement token.

#### Setup Widget Theme

This sections explains how to control the color theme of the widget rendered inside a Storyly Placement, so it blends in with your app's light or dark appearance.

You need to use `setTheme` to set the widget color theme via `STRPlacementConfig`.

```kotlin Kotlin
config = STRPlacementConfig.Builder()
    .setTheme(STRTheme.DARK) // or STRTheme.LIGHT (default)
    .build(token = "<your_placement_token>")
```

`STRTheme` is an enum with two values:

| Value            | Appearance            | When to use                                          |
| ---------------- | --------------------- | ---------------------------------------------------- |
| `STRTheme.LIGHT` | Light theme (default) | Hosts with a light background, or app in light mode. |
| `STRTheme.DARK`  | Dark theme            | Hosts with a dark background, or app in dark mode.   |

### StorylyPlacement

This is the composable that actually changes widgets in and out. You place it in your Compose layout, pass it the `STRPlacementDataProvider` instance, and size it with a `Modifier`.

```kotlin Kotlin
@Composable
fun HomeScreen() {
    val provider = viewModel<PlacementViewModel>().provider

    StorylyPlacement(
        dataProvider = provider,
        modifier = Modifier
            .fillMaxWidth()
            .height(220.dp),
    )
}
```

| Parameter         | Required | Description                                                                            |
| ----------------- | -------- | -------------------------------------------------------------------------------------- |
| `dataProvider`    | Yes      | The caller-owned `STRPlacementDataProvider` supplying placement data.                  |
| `modifier`        | No       | The `Modifier` applied to the placement. It **must** give the placement a size.        |
| `listener`        | No       | `STRListener` for ready, visibility, action-click, event and failure callbacks.        |
| `productListener` | No       | `STRProductListener` for shoppable-content callbacks (cart, wishlist, product events). |

> 🚧 Warning
>
> The placement has **no intrinsic size**. If the `modifier` does not give it a width and a height, nothing will be rendered. Either give it a fixed size, or derive its height from the ratio reported by `onWidgetReady` — see [Placement Size Handling](#placement-size-handling).

> 📘 Info
>
> `StorylyPlacement` renders an empty placeholder in `@Preview`. The widget is a platform `View`, which Compose's inspection mode cannot draw, so a blank preview is expected — run the app to see the placement.

### STRListener

Storyly Placement provides several listener callbacks that allow your application to react to changes in widget state, user interactions, and analytics events.

To observe and respond to these behaviors, you must pass an `STRListener` to the `StorylyPlacement` composable.

> 📘 Info
>
> ### onWidgetReady
>
> This callback is triggered when the active widget ready to render. It gives a width-to-height ratio for the best rendering of view.
>
> ### onVisibilityChange
>
> This callback is triggered when the placement view visibility should change based on widget data availability.
>
> ### onActionClicked
>
> This callback is triggered when the user interacts with the widget’s action area (e.g., swipe-up or action button).

Callbacks are delivered off the composition, so they need somewhere to write into. The recommended pattern is a small snapshot-state holder that the listener writes and your layout reads:

```kotlin Kotlin
class PlacementUiState {
    /** Width-to-height ratio reported by onWidgetReady; null until a widget is ready. */
    var ratio by mutableStateOf<Float?>(null)

    /** Last value from onVisibilityChange — true while the placement has content to show. */
    var visible by mutableStateOf(false)

    /** True once onWidgetReady has fired, i.e. ratio can be trusted. */
    var ready by mutableStateOf(false)
}

@Composable
fun rememberPlacementListener(state: PlacementUiState): STRListener = remember(state) {
    object : STRListener {
        override fun onWidgetReady(widget: STRWidgetController, ratio: Float) {
            state.ratio = ratio
            state.ready = true
        }

        override fun onVisibilityChange(widget: STRWidgetController?, isVisible: Boolean) {
            state.visible = isVisible
            if (!isVisible) {
                state.ready = false
                state.ratio = null
            }
        }

        override fun onActionClicked(widget: STRWidgetController, url: String, payload: STRPayload) {
            // See Placement Action Handling below.
        }
    }
}
```

> 📘 Tip
>
> Wrap the listener in `remember` so a new instance is not allocated on every recomposition. `StorylyPlacement` re-applies the listener on each recomposition, so the placement always holds the latest one you pass.

#### Placement Size Handling

This section explains how to size the placement whenever the widget changes its size ratio.

The `onWidgetReady` callback notifies your application when the widget is ready to render with required ratio, allowing you to calculate and update the correct layout for the placement. In Compose you feed that ratio straight into `Modifier.aspectRatio`, and collapse the placement to zero height until it arrives so an empty placement reserves no space.

```kotlin Kotlin
@Composable
fun HomeScreen() {
    val provider = viewModel<PlacementViewModel>().provider
    val state = remember { PlacementUiState() }
    val listener = rememberPlacementListener(state)

    val ratio = state.ratio
    val sizeModifier = if (state.ready && ratio != null && ratio > 0f && ratio.isFinite()) {
        Modifier.aspectRatio(ratio)
    } else {
        Modifier.height(0.dp)
    }

    StorylyPlacement(
        dataProvider = provider,
        modifier = Modifier.fillMaxWidth().then(sizeModifier),
        listener = listener,
    )
}
```

> 📘 Tip
>
> Inside a `LazyColumn` or any list, reserve a fixed height instead of collapsing to `0.dp` until the widget is ready. Otherwise the list jumps when the placement appears.

> 🚧 Warning
>
> You must test with different widget types in the Dashboard's Placement page to verify that dynamic sizing behaves as expected. You can use this as a verification step in your initialization.

#### Placement Action Handling

This section shows how to handle Swipe Up and Action Button clicks from user.

The redirection needs to be handled by the application itself when the end-user clicks any action in the content. In order to handle this action, you must override `onActionClicked` function in your `STRListener`. You can handle the action using the following code example:

```kotlin Kotlin
override fun onActionClicked(
    widget: STRWidgetController,
    url: String,
    payload: STRPayload
) {
    Log.d(logTag, "onActionClicked: url=$url, payload=$payload")
    // Use widget.pause(null) to temporarily suspend widget activity

    // Implement navigation via URL or handle product-specific actions here

    // Use widget.resume(null) to restore functionality once navigation is complete
}
```

> 🚧 Warning
>
> For StoryBar and VideoFeed, the destination **must be its own Activity**. Their fullscreen player fills the host Activity's window, so navigating to another Compose destination inside the same Activity renders *behind* the player and the user never sees it.

> 🚧 Warning
>
> Please confirm `onActionClicked` callback is triggered upon any action in the content. Make sure your app navigates to correctly and check the logs to validate.

#### Placement Visibility Handling

This section explains how to handle the visibility of the placement based on whether there is a widget to display.

The `onVisibilityChange` callback notifies your application when the placement's visibility should change. This happens when the widget data is successfully loaded (isVisible=true) or when there is no data or a load failure (isVisible=false).

```kotlin Kotlin
override fun onVisibilityChange(
    widget: STRWidgetController?,
    isVisible: Boolean,
) {
    // Update the visibility of the placement to match the state of the isVisible flag
}
```

> 📘 Best Practices
>
> * You must own the `STRPlacementDataProvider` outside the composition (`ViewModel` or `remember`) so it is not rebuilt on recomposition
> * You must give the placement a size via `modifier` — it has no intrinsic size
> * You must honor `onWidgetReady` for responsive UI
> * You must honor `onVisibilityChange` for visibility of placement
> * You must handle `onActionClicked` for navigation for action of end-user, and open a separate Activity for StoryBar and VideoFeed
> * You should `remember` your listeners so they are not reallocated on every recomposition
> * You should handle `setTheme` for consistent color theme look with your application.