Skip to main content

Jetpack Compose Integration

The StorifyMe Android SDK provides native Jetpack Compose support, allowing you to integrate stories seamlessly into your Compose-based applications. This integration offers a modern, declarative API that feels natural in Compose projects.

Why Use Native Compose Integration?

The native Compose integration provides several advantages over the traditional View-based approach:

  • Declarative API: Use composable functions that fit naturally into your Compose UI
  • Automatic Lifecycle Management: The widget handles lifecycle automatically with Compose's lifecycle
  • Type-Safe Configuration: Use remember* helper functions for configuration that survives recomposition
  • Per-Widget Callbacks: Set event callbacks directly on each widget instead of using a global listener
  • Compose-Friendly Patterns: Works seamlessly with State, remember, LazyColumn, and other Compose features

Prerequisites

Before integrating StoriesView in Compose, ensure you have:

  1. SDK Version: StorifyMe Android SDK version 2.5.7 or higher
  2. Compose Version: Jetpack Compose BOM 2024.01.00 or higher, or individual Compose dependencies at version 1.6.0+
  3. SDK Initialization: StorifyMe SDK must be initialized before rendering StoriesView
Required Initialization

You must initialize the StorifyMe SDK before using any StoriesView composable. Typically this is done in your Application class or MainActivity's onCreate before setContent.

main.kt
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()

StorifyMe.init(
context = this,
apiKey = "your-api-key",
accountId = "your-account-id",
environment = StorifyMeEnv.EU // or StorifyMeEnv.US, StorifyMeEnv.DEV
)
}
}

Basic Usage

Simple Widget

The most basic way to add StoriesView to your Compose app is to provide just the widget ID:

main.kt
import com.storify.android_sdk.ui.compose.StoriesView

@Composable
fun MyScreen() {
StoriesView(
widgetId = 12345,
modifier = Modifier.fillMaxWidth()
)
}
info

Make sure to replace 12345 with your actual widget ID. You can find your widget ID in the widget share dialog on the StorifyMe platform.

Widget with Padding

Add padding around the widget using standard Compose modifiers:

main.kt
@Composable
fun MyScreen() {
Column {
StoriesView(
widgetId = 12345,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp)
)
}
}

Multiple Widgets

You can easily add multiple widgets to a screen:

main.kt
@Composable
fun MyScreen() {
LazyColumn {
item {
Text("Featured Stories", style = MaterialTheme.typography.headlineMedium)
}

item {
StoriesView(
widgetId = 12345,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}

item {
Text("Trending Stories", style = MaterialTheme.typography.headlineMedium)
}

item {
StoriesView(
widgetId = 67890,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
}
}

Simple Configuration

You can configure the widget using rememberStoriesViewConfig():

main.kt
import com.storify.android_sdk.ui.compose.controller.rememberStoriesViewConfig

@Composable
fun MyScreen() {
val config = rememberStoriesViewConfig(
language = "en",
direction = StorifyMeContentDirection.LTR,
segments = listOf("premium", "vip")
)

StoriesView(
widgetId = 12345,
config = config,
modifier = Modifier.fillMaxWidth()
)
}
tip

The remember* helper functions ensure your configuration survives recomposition. Always use these helpers instead of creating configuration objects directly.

Configuration Options

The StorifyMe Compose integration provides several configuration options to customize widget behavior, appearance, and media handling. All configuration uses remember* helper functions to ensure stability across recompositions.

Widget Configuration

Use rememberStoriesViewConfig() to configure language, direction, segments, and query parameters:

Language

Set the language for the widget content:

main.kt
val config = rememberStoriesViewConfig(
language = "en" // or "es", "fr", "de", etc.
)

StoriesView(
widgetId = 12345,
config = config,
modifier = Modifier.fillMaxWidth()
)

Direction (RTL/LTR)

Control content direction for right-to-left languages:

main.kt
import com.storify.android_sdk.domain.enums.StorifyMeContentDirection

val config = rememberStoriesViewConfig(
direction = StorifyMeContentDirection.RTL // or LTR
)

StoriesView(
widgetId = 12345,
config = config,
modifier = Modifier.fillMaxWidth()
)

Segments (Tags)

Filter stories by segments/tags:

main.kt
val config = rememberStoriesViewConfig(
segments = listOf("premium", "vip", "new-users")
)

StoriesView(
widgetId = 12345,
config = config,
modifier = Modifier.fillMaxWidth()
)
tip

Segments allow you to show different stories to different user groups. You can manage segments in the StorifyMe platform.

Query Parameters

Add custom query parameters to widget requests:

main.kt
val queryParams = mapOf(
"source" to "mobile-app",
"campaign" to "summer-sale"
)

val config = rememberStoriesViewConfig(
queryParameters = queryParams
)

StoriesView(
widgetId = 12345,
config = config,
modifier = Modifier.fillMaxWidth()
)

Combined Configuration

Combine multiple configuration options:

main.kt
val config = rememberStoriesViewConfig(
language = "en",
direction = StorifyMeContentDirection.LTR,
segments = listOf("premium", "vip"),
queryParameters = mapOf("source" to "mobile")
)

StoriesView(
widgetId = 12345,
config = config,
modifier = Modifier.fillMaxWidth()
)

Media Options

Control how media (GIFs and videos) is displayed in story posters using rememberMediaOptions():

Video Posters

Enable or disable video posters in the widget:

main.kt
import com.storify.android_sdk.ui.compose.config.rememberMediaOptions

val mediaOptions = rememberMediaOptions(
videoPosterEnabled = true // Default: false
)

StoriesView(
widgetId = 12345,
mediaOptions = mediaOptions,
modifier = Modifier.fillMaxWidth()
)
Default Behavior

By default, video posters are disabled to reduce data usage and improve performance. Enable them for richer previews.

GIF Posters

Control GIF poster display:

main.kt
val mediaOptions = rememberMediaOptions(
gifPosterEnabled = false // Default: true
)

StoriesView(
widgetId = 12345,
mediaOptions = mediaOptions,
modifier = Modifier.fillMaxWidth()
)
Default Behavior

GIF posters are enabled by default. Disable them to reduce data usage on slower connections.

Combined Media Options

Configure both video and GIF posters:

main.kt
val mediaOptions = rememberMediaOptions(
videoPosterEnabled = true,
gifPosterEnabled = false
)

StoriesView(
widgetId = 12345,
mediaOptions = mediaOptions,
modifier = Modifier.fillMaxWidth()
)

Behavior Options

Customize story playback, audio, fullscreen mode, and URL handling using rememberBehaviorOptions():

Fullscreen Mode

Enable or disable fullscreen display for stories:

main.kt
import com.storify.android_sdk.ui.compose.config.rememberBehaviorOptions

val behaviorOptions = rememberBehaviorOptions(
fullscreenEnabled = true // Default: false
)

StoriesView(
widgetId = 12345,
behaviorOptions = behaviorOptions,
modifier = Modifier.fillMaxWidth()
)
Fullscreen for Shorts

Fullscreen mode is particularly useful for shorts content where immersion enhances the viewing experience.

Audio Behavior

Control how audio state persists across stories:

main.kt
import com.storify.android_sdk.domain.enums.StoryAudioBehaviour
import com.storify.android_sdk.domain.enums.StoryAudioState

val behaviorOptions = rememberBehaviorOptions(
audioBehaviour = StoryAudioBehaviour.APPLY_LAST_USER_CHANGE_FOR_ALL_FUTURE_STORIES,
audioDefaultState = StoryAudioState.MUTED
)

StoriesView(
widgetId = 12345,
behaviorOptions = behaviorOptions,
modifier = Modifier.fillMaxWidth()
)

Audio Behavior Options:

  • APPLY_LAST_USER_CHANGE_FOR_ALL_FUTURE_STORIES - Remember user's audio preference across all stories (default)
  • APPLY_CHANGE_FOR_SINGLE_STORY - Audio changes only affect the current story
  • APPLY_CHANGE_FOR_PRESENTED_STORIES - Audio changes affect currently loaded stories only

Audio Default States:

  • MUTED - Stories start muted
  • UNMUTED - Stories start with audio playing

Unmute on Volume Change

Automatically unmute stories when user changes device volume:

main.kt
val behaviorOptions = rememberBehaviorOptions(
unmuteOnOutputVolumeChange = true // Default: false
)

StoriesView(
widgetId = 12345,
behaviorOptions = behaviorOptions,
modifier = Modifier.fillMaxWidth()
)

Playback Behavior

Control how story playback resumes:

main.kt
import com.storify.android_sdk.domain.enums.StoryPlaybackBehaviour

val behaviorOptions = rememberBehaviorOptions(
playbackBehaviour = StoryPlaybackBehaviour.ALWAYS_RESUME_STORY_WHERE_STOPPED
)

StoriesView(
widgetId = 12345,
behaviorOptions = behaviorOptions,
modifier = Modifier.fillMaxWidth()
)

Playback Behavior Options:

  • ALWAYS_RESUME_STORY_WHERE_STOPPED - Resume where user left off (default)
  • RESTART_STORIES_ON_APP_LAUNCH - Restart stories on each app launch
  • RESTART_STORIES_WHEN_OPEN - Restart stories each time they're opened

Story Item Pulse Animation

Enable or disable the pulse animation on story items:

main.kt
val behaviorOptions = rememberBehaviorOptions(
storyItemPulseAnimationEnabled = true // Default: true
)

StoriesView(
widgetId = 12345,
behaviorOptions = behaviorOptions,
modifier = Modifier.fillMaxWidth()
)

URL Presentation

Control how URLs open within stories:

main.kt
import com.storify.android_sdk.domain.enums.StorifyMeURLPresentationBehaviour

val behaviorOptions = rememberBehaviorOptions(
urlPresentationBehaviour = StorifyMeURLPresentationBehaviour.IN_APP_BROWSER
)

StoriesView(
widgetId = 12345,
behaviorOptions = behaviorOptions,
modifier = Modifier.fillMaxWidth()
)

URL Presentation Options:

  • EXTERNAL_BROWSER - Open URLs in external browser (default)
  • IN_APP_BROWSER - Open URLs in in-app browser

Combined Behavior Options

Configure multiple behavior options together:

main.kt
val behaviorOptions = rememberBehaviorOptions(
fullscreenEnabled = true,
storyItemPulseAnimationEnabled = true,
audioBehaviour = StoryAudioBehaviour.APPLY_LAST_USER_CHANGE_FOR_ALL_FUTURE_STORIES,
audioDefaultState = StoryAudioState.MUTED,
unmuteOnOutputVolumeChange = true,
playbackBehaviour = StoryPlaybackBehaviour.ALWAYS_RESUME_STORY_WHERE_STOPPED,
urlPresentationBehaviour = StorifyMeURLPresentationBehaviour.IN_APP_BROWSER
)

StoriesView(
widgetId = 12345,
behaviorOptions = behaviorOptions,
modifier = Modifier.fillMaxWidth()
)

Progress Options

Control progress bar visibility using rememberProgressOptions():

main.kt
import com.storify.android_sdk.ui.compose.config.rememberProgressOptions

val progressOptions = rememberProgressOptions(
showProgressBar = false // Default: true
)

StoriesView(
widgetId = 12345,
progressOptions = progressOptions,
modifier = Modifier.fillMaxWidth()
)
Default Behavior

The progress bar is enabled by default to show story playback progress. Disable it for a cleaner, minimalist look.

Combining All Configuration Options

You can combine all configuration options in a single StoriesView:

main.kt
@Composable
fun FullyConfiguredWidget() {
val config = rememberStoriesViewConfig(
language = "en",
direction = StorifyMeContentDirection.LTR,
segments = listOf("premium", "vip")
)

val mediaOptions = rememberMediaOptions(
videoPosterEnabled = true,
gifPosterEnabled = true
)

val behaviorOptions = rememberBehaviorOptions(
fullscreenEnabled = true,
audioBehaviour = StoryAudioBehaviour.APPLY_LAST_USER_CHANGE_FOR_ALL_FUTURE_STORIES,
audioDefaultState = StoryAudioState.MUTED,
playbackBehaviour = StoryPlaybackBehaviour.ALWAYS_RESUME_STORY_WHERE_STOPPED,
urlPresentationBehaviour = StorifyMeURLPresentationBehaviour.IN_APP_BROWSER
)

val progressOptions = rememberProgressOptions(
showProgressBar = true
)

StoriesView(
widgetId = 12345,
config = config,
mediaOptions = mediaOptions,
behaviorOptions = behaviorOptions,
progressOptions = progressOptions,
modifier = Modifier.fillMaxWidth()
)
}

Event Handling

The Compose integration provides grouped callback interfaces for handling widget events. Unlike the View-based API that uses a global StorifyMe.instance.eventListener, Compose allows you to set callbacks directly on each widget.

Per-Widget vs Global Callbacks

In Compose, callbacks are set per-widget, giving you fine-grained control. You can still use the global StorifyMe.instance.eventListener for app-wide event tracking if needed.

Load Events

Handle widget loading success and failure with StorifyMeLoadEventCallbacks:

main.kt
import com.storify.android_sdk.ui.compose.callbacks.StorifyMeLoadEventCallbacks

@Composable
fun MyScreen() {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }

val loadCallbacks = StorifyMeLoadEventCallbacks(
onLoad = { widgetId, stories ->
Log.d("StoriesView", "Loaded ${stories.size} stories for widget $widgetId")
scope.launch {
snackbarHostState.showSnackbar("Loaded ${stories.size} stories")
}
},
onFail = { error ->
Log.e("StoriesView", "Failed to load: ${error.message}")
scope.launch {
snackbarHostState.showSnackbar("Error: ${error.message}")
}
}
)

Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) {
StoriesView(
widgetId = 12345,
loadEventCallbacks = loadCallbacks,
modifier = Modifier.fillMaxWidth()
)
}
}

onLoad Parameters:

  • widgetId: Long - The ID of the widget that loaded
  • stories: List<StoryWithSeen> - List of loaded stories with seen status

onFail Parameters:

  • error: StorifyMeError - Error object containing message and type

Lifecycle Events

Track story lifecycle with StorifyMeLifecycleCallbacks:

main.kt
import com.storify.android_sdk.ui.compose.callbacks.StorifyMeLifecycleCallbacks

@Composable
fun MyScreen() {
val lifecycleCallbacks = StorifyMeLifecycleCallbacks(
onStoryOpened = { story, index ->
Log.d("StoriesView", "Opened story: ${story.story.name} at index $index")
// Track analytics
},
onStoryClosed = { story ->
Log.d("StoriesView", "Closed story: ${story.story.name}")
// Update UI state
},
onStoryFinished = { story, index ->
Log.d("StoriesView", "Finished story: ${story.story.name} at index $index")
// Track completion
}
)

StoriesView(
widgetId = 12345,
lifecycleCallbacks = lifecycleCallbacks,
modifier = Modifier.fillMaxWidth()
)
}

Available Callbacks:

  • onStoryOpened(story, index) - Triggered when user opens a story
  • onStoryClosed(story) - Triggered when user closes the story viewer
  • onStoryFinished(story, index) - Triggered when a story completes playback

Interaction Events

Handle user interactions with StorifyMeInteractionCallbacks:

main.kt
import com.storify.android_sdk.ui.compose.callbacks.StorifyMeInteractionCallbacks
import org.json.JSONObject

@Composable
fun MyScreen() {
val interactionCallbacks = StorifyMeInteractionCallbacks(
onAction = { type, data ->
Log.d("StoriesView", "Action: $type")
// Handle user actions (button clicks, quiz answers, etc.)
when (type) {
"BUTTON" -> {
val buttonData = data?.optJSONObject("data")
val url = buttonData?.optString("value")
Log.d("StoriesView", "Button clicked: $url")
}
"QUIZ_ANSWER" -> {
val answer = data?.optJSONObject("data")
Log.d("StoriesView", "Quiz answer: $answer")
}
}
},
onEvent = { type, data ->
Log.d("StoriesView", "Event: $type")
// Track analytics events
// Send to your analytics platform (e.g., Mixpanel, Amplitude, etc.)
},
onStoryShared = { story ->
Log.d("StoriesView", "Story shared: ${story.story.name}")
// Track sharing analytics
}
)

StoriesView(
widgetId = 12345,
interactionCallbacks = interactionCallbacks,
modifier = Modifier.fillMaxWidth()
)
}

Available Callbacks:

  • onAction(type, data) - Triggered when user interacts with engaging elements (buttons, quizzes, polls, etc.)
  • onEvent(type, data) - Triggered for analytics events (slide views, element interactions, etc.)
  • onStoryShared(story) - Triggered when user shares a story
Analytics Integration

The onEvent callback is ideal for integrating with analytics platforms. However, you can also configure analytics integrations directly in the StorifyMe admin panel.

Shopping Events

Track e-commerce actions with StorifyMeShoppingCallbacks:

main.kt
import com.storify.android_sdk.ui.compose.callbacks.StorifyMeShoppingCallbacks

@Composable
fun MyScreen() {
val shoppingCallbacks = StorifyMeShoppingCallbacks(
onShopping = { type, data ->
Log.d("StoriesView", "Shopping event: $type")

when (type) {
"CART_UPDATED" -> {
val cartItems = data?.optJSONArray("items")
Log.d("StoriesView", "Cart updated: $cartItems")
// Update your app's cart state
}
"CART_ITEM_ADDED" -> {
val product = data?.optJSONObject("product")
val productId = product?.optString("id")
Log.d("StoriesView", "Product added: $productId")
// Sync with your e-commerce system
}
"CART_ITEM_REMOVED" -> {
val productId = data?.optString("productId")
Log.d("StoriesView", "Product removed: $productId")
// Remove from your app's cart
}
"CHECKOUT" -> {
val cartData = data?.optJSONObject("cart")
Log.d("StoriesView", "Checkout initiated")
// Navigate to checkout flow
}
}
}
)

StoriesView(
widgetId = 12345,
shoppingCallbacks = shoppingCallbacks,
modifier = Modifier.fillMaxWidth()
)
}

Supported Shopping Event Types:

  • CART_UPDATED - Triggered whenever the cart changes
  • CART_ITEM_ADDED - Triggered when a product is added to the cart
  • CART_ITEM_REMOVED - Triggered when a product is removed from the cart
  • CHECKOUT - Triggered when user clicks the checkout button
E-commerce Integration

Use shopping callbacks to synchronize StorifyMe's story cart with your app's shopping cart and checkout system.

Intercept and customize link handling with StorifyMeLinkInterceptionCallbacks:

main.kt
import com.storify.android_sdk.ui.compose.callbacks.StorifyMeLinkInterceptionCallbacks
import com.storify.android_sdk.domain.enums.StorifyMeLinkTriggerCompletion
import com.storify.android_sdk.domain.enums.StorifyMeStoryDeeplinkTriggerCompletion

@Composable
fun MyScreen() {
val linkCallbacks = StorifyMeLinkInterceptionCallbacks(
onLinkOpenTriggered = { url, completion ->
Log.d("StoriesView", "Link triggered: $url")

// Check if this is an internal deep link
if (url.startsWith("myapp://")) {
// Handle internally - navigate using your app's navigation
// navController.navigate(...)

// Prevent default link opening
completion(StorifyMeLinkTriggerCompletion.IGNORE_PRESENTING_LINK)
} else {
// Let SDK handle external links
completion(StorifyMeLinkTriggerCompletion.OPEN_LINK_BY_DEFAULT)
}
},
onStoryDeeplinkTriggered = { story, completion ->
Log.d("StoriesView", "Story deeplink: ${story.story.handle}")

// Check if user has permission to view this story
val hasPermission = checkUserPermission()

if (!hasPermission) {
// Don't open the story
completion(StorifyMeStoryDeeplinkTriggerCompletion.IGNORE_PRESENTING_STORY)
// Show permission dialog
} else {
// Open story normally
completion(StorifyMeStoryDeeplinkTriggerCompletion.OPEN_STORY_BY_DEFAULT)
}
}
)

StoriesView(
widgetId = 12345,
linkInterceptionCallbacks = linkCallbacks,
modifier = Modifier.fillMaxWidth()
)
}

Available Callbacks:

  • onLinkOpenTriggered(url, completion) - Intercept link clicks from CTAs, product tags, swipe-ups
  • onStoryDeeplinkTriggered(story, completion) - Intercept story deep link opening

Completion Options for Link Opening:

  • OPEN_LINK_BY_DEFAULT - Let SDK handle the link (open in browser)
  • IGNORE_PRESENTING_LINK - Prevent opening, handle manually in your app

Completion Options for Story Deeplink:

  • OPEN_STORY_BY_DEFAULT - Open the story normally
  • IGNORE_PRESENTING_STORY - Don't open the story (useful for permission checks)
Deep Link Integration

Use onLinkOpenTriggered to handle your app's deep links (e.g., myapp://product/123) and navigate within your app instead of opening a browser.

Combining Multiple Callbacks

You can use multiple callback types together:

main.kt
@Composable
fun FullyTrackedWidget() {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }

val loadCallbacks = StorifyMeLoadEventCallbacks(
onLoad = { widgetId, stories ->
Log.d("Widget", "Loaded ${stories.size} stories")
},
onFail = { error ->
scope.launch {
snackbarHostState.showSnackbar("Failed to load: ${error.message}")
}
}
)

val lifecycleCallbacks = StorifyMeLifecycleCallbacks(
onStoryOpened = { story, index ->
// Track story view in analytics
analytics.logEvent("story_viewed", mapOf(
"story_id" to story.story.id.toString(),
"story_name" to story.story.name
))
},
onStoryClosed = { story ->
Log.d("Widget", "Closed: ${story.story.name}")
}
)

val interactionCallbacks = StorifyMeInteractionCallbacks(
onAction = { type, data ->
// Track user interactions
analytics.logEvent("story_interaction", mapOf(
"type" to type
))
}
)

Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) {
StoriesView(
widgetId = 12345,
loadEventCallbacks = loadCallbacks,
lifecycleCallbacks = lifecycleCallbacks,
interactionCallbacks = interactionCallbacks,
modifier = Modifier
.fillMaxWidth()
.padding(it)
)
}
}

Controller Usage

For advanced control over the widget, use rememberStoriesViewController() to access imperative operations and observe widget state.

Creating a Controller

main.kt
import com.storify.android_sdk.ui.compose.controller.rememberStoriesViewController

@Composable
fun MyScreen() {
val controller = rememberStoriesViewController()

StoriesView(
widgetId = 12345,
controller = controller,
modifier = Modifier.fillMaxWidth()
)
}

Observing Widget State

The controller provides state flows for observing widget properties and server segments:

main.kt
@Composable
fun MyScreen() {
val controller = rememberStoriesViewController()

// Observe widget properties (size, layout info)
val widgetProperties by controller.widgetProperties.collectAsState()

// Observe server segments
val serverSegments by controller.serverSegments.collectAsState()

Column {
// Display widget info
widgetProperties?.let { props ->
Text(
text = "Widget size: ${props.storyWidth}x${props.storyHeight}px",
style = MaterialTheme.typography.bodySmall
)
}

// Display segments
if (serverSegments.isNotEmpty()) {
Text(
text = "Active segments: ${serverSegments.joinToString()}",
style = MaterialTheme.typography.bodySmall
)
}

StoriesView(
widgetId = 12345,
controller = controller,
modifier = Modifier.fillMaxWidth()
)
}
}

Reload Widget

Programmatically reload the widget:

main.kt
@Composable
fun MyScreen() {
val controller = rememberStoriesViewController()

Column {
Button(onClick = {
controller.load() // Reload the widget
}) {
Text("Reload Stories")
}

StoriesView(
widgetId = 12345,
controller = controller,
modifier = Modifier.fillMaxWidth()
)
}
}

Manual Pagination

Load more stories manually (useful for custom "Load More" buttons):

main.kt
@Composable
fun MyScreen() {
val controller = rememberStoriesViewController()
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }

Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { padding ->
Column(modifier = Modifier.padding(padding)) {
StoriesView(
widgetId = 12345,
controller = controller,
modifier = Modifier.fillMaxWidth()
)

Button(
onClick = {
val hasMore = controller.loadMore()
scope.launch {
if (hasMore) {
snackbarHostState.showSnackbar("Loading more stories...")
} else {
snackbarHostState.showSnackbar("No more stories available")
}
}
},
modifier = Modifier.align(Alignment.CenterHorizontally)
) {
Text("Load More")
}
}
}
}
Automatic Pagination

By default, StoriesView handles pagination automatically when users scroll. Manual pagination is only needed for custom UI implementations.

Open Story by Position

Programmatically open a story by its index:

main.kt
@Composable
fun MyScreen() {
val controller = rememberStoriesViewController()

Column {
Button(onClick = {
controller.openWidgetStoryByPosition(0) { result ->
when (result) {
StorifyMeWidgetStoryNavigatorExecutionResult.SUCCESS -> {
Log.d("Widget", "Story opened successfully")
}
StorifyMeWidgetStoryNavigatorExecutionResult.NOT_LOADED -> {
Log.e("Widget", "Widget not loaded yet")
}
StorifyMeWidgetStoryNavigatorExecutionResult.STORY_NOT_FOUND -> {
Log.e("Widget", "Story not found")
}
}
}
}) {
Text("Open First Story")
}

StoriesView(
widgetId = 12345,
controller = controller,
modifier = Modifier.fillMaxWidth()
)
}
}

Open Story by Handle

Open a story using its unique handle:

main.kt
@Composable
fun MyScreen() {
val controller = rememberStoriesViewController()

Column {
Button(onClick = {
controller.openWidgetStoryByHandle("summer-sale-2024") { result ->
if (result == StorifyMeWidgetStoryNavigatorExecutionResult.SUCCESS) {
Log.d("Widget", "Story opened by handle")
}
}
}) {
Text("Open Summer Sale Story")
}

StoriesView(
widgetId = 12345,
controller = controller,
modifier = Modifier.fillMaxWidth()
)
}
}

Open Story by ID

Open a story using its ID:

main.kt
@Composable
fun MyScreen() {
val controller = rememberStoriesViewController()

Column {
Button(onClick = {
controller.openWidgetStoryById(12345L) { result ->
if (result == StorifyMeWidgetStoryNavigatorExecutionResult.SUCCESS) {
Log.d("Widget", "Story opened by ID")
}
}
}) {
Text("Open Story by ID")
}

StoriesView(
widgetId = 12345,
controller = controller,
modifier = Modifier.fillMaxWidth()
)
}
}

Advanced Features

Custom Data & Personalization

Dynamic Data (Integration Variables)

Pass dynamic data to stories for personalization:

main.kt
import org.json.JSONObject

@Composable
fun PersonalizedWidget(userId: String, userName: String) {
val dynamicData = remember(userId, userName) {
mapOf(
"integration-id-1" to JSONObject().apply {
put("userName", userName)
put("userId", userId)
put("accountTier", "premium")
put("points", 1250)
},
"integration-id-2" to JSONObject().apply {
put("cartValue", 99.99)
put("itemCount", 3)
}
)
}

StoriesView(
widgetId = 12345,
dynamicData = dynamicData,
modifier = Modifier.fillMaxWidth()
)
}
Integration Variables

Create integration variables in the StorifyMe platform, then pass their values dynamically from your app. This enables personalized content for each user.

Segment User ID

Pass a user ID for CDP audience segmentation:

main.kt
@Composable
fun SegmentedWidget(userId: String) {
StoriesView(
widgetId = 12345,
segmentUserId = userId, // For audience segment resolution
modifier = Modifier.fillMaxWidth()
)
}

Pull-to-Refresh Integration

Integrate with Material 3's PullToRefreshBox:

main.kt
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.pulltorefresh.PullToRefreshBox

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RefreshableWidget() {
val controller = rememberStoriesViewController()
var isRefreshing by remember { mutableStateOf(false) }

PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
isRefreshing = true
controller.load()
}
) {
StoriesView(
widgetId = 12345,
controller = controller,
loadEventCallbacks = StorifyMeLoadEventCallbacks(
onLoad = { _, _ ->
isRefreshing = false
},
onFail = { _ ->
isRefreshing = false
}
),
modifier = Modifier.fillMaxWidth()
)
}
}

Height Caching for LazyColumn

When using multiple widgets in a LazyColumn, enable height caching to prevent scroll jumping:

main.kt
@Composable
fun OptimizedList() {
LazyColumn {
items(5) { index ->
StoriesView(
widgetId = (100L + index),
enableHeightCache = true, // Prevents scroll jumping
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
}
}
How Height Caching Works

The SDK caches the measured height of each widget in the session. When widgets are disposed and recreated (during scrolling), the cached height provides a size hint to LazyColumn, preventing unexpected scroll position changes.

Multiple Widgets Management

Manage multiple widgets with different configurations:

main.kt
@Composable
fun MultiWidgetScreen() {
LazyColumn {
// Featured stories - premium users only
item {
Text(
"Featured Stories",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(16.dp)
)
}

item {
val config = rememberStoriesViewConfig(
segments = listOf("premium", "featured")
)

StoriesView(
widgetId = 101,
config = config,
enableHeightCache = true,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
)
}

// Trending stories - all users
item {
Text(
"Trending Now",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(16.dp)
)
}

item {
val config = rememberStoriesViewConfig(
segments = listOf("trending")
)

StoriesView(
widgetId = 102,
config = config,
enableHeightCache = true,
lifecycleCallbacks = StorifyMeLifecycleCallbacks(
onStoryOpened = { story, _ ->
// Track trending story views
analytics.logEvent("trending_story_viewed")
}
),
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
)
}

// Regional stories
item {
Text(
"Local Stories",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(16.dp)
)
}

item {
val config = rememberStoriesViewConfig(
segments = listOf("local", "en-US")
)

StoriesView(
widgetId = 103,
config = config,
enableHeightCache = true,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp)
)
}
}
}

Practical Examples

Here are complete, real-world examples demonstrating common use cases:

Example 1: Basic Widget

The simplest integration:

main.kt
@Composable
fun BasicWidget() {
StoriesView(
widgetId = 12345,
modifier = Modifier.fillMaxWidth()
)
}

Example 2: Widget with Load Tracking

Track story loading and display count:

main.kt
@Composable
fun TrackedWidget() {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }
var loadedCount by remember { mutableIntStateOf(0) }

Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { padding ->
Column(modifier = Modifier.padding(padding)) {
if (loadedCount > 0) {
Text(
text = "$loadedCount stories available",
modifier = Modifier.padding(8.dp)
)
}

StoriesView(
widgetId = 12345,
loadEventCallbacks = StorifyMeLoadEventCallbacks(
onLoad = { _, stories ->
loadedCount = stories.size
},
onFail = { error ->
scope.launch {
snackbarHostState.showSnackbar("Error: ${error.message}")
}
}
),
modifier = Modifier.fillMaxWidth()
)
}
}
}

Example 3: Widget with Analytics

Track story interactions with your analytics platform:

main.kt
@Composable
fun AnalyticsWidget(userId: String) {
val config = rememberStoriesViewConfig(
language = "en",
segments = listOf("premium"),
queryParameters = mapOf("userId" to userId)
)

val lifecycleCallbacks = StorifyMeLifecycleCallbacks(
onStoryOpened = { story, index ->
analytics.logEvent("story_opened", bundleOf(
"story_id" to story.story.id.toString(),
"story_name" to story.story.name,
"position" to index
))
},
onStoryFinished = { story, _ ->
analytics.logEvent("story_completed", bundleOf(
"story_id" to story.story.id.toString()
))
}
)

StoriesView(
widgetId = 12345,
config = config,
lifecycleCallbacks = lifecycleCallbacks,
modifier = Modifier.fillMaxWidth()
)
}

Handle app deep links and product navigation:

main.kt
@Composable
fun DeepLinkWidget(navController: NavController) {
val linkCallbacks = StorifyMeLinkInterceptionCallbacks(
onLinkOpenTriggered = { url, completion ->
when {
url.startsWith("myapp://product/") -> {
val productId = url.substringAfter("myapp://product/")
navController.navigate("product/$productId")
completion(StorifyMeLinkTriggerCompletion.IGNORE_PRESENTING_LINK)
}
url.startsWith("myapp://") -> {
navController.navigate(url.substringAfter("myapp://"))
completion(StorifyMeLinkTriggerCompletion.IGNORE_PRESENTING_LINK)
}
else -> {
completion(StorifyMeLinkTriggerCompletion.OPEN_LINK_BY_DEFAULT)
}
}
}
)

StoriesView(
widgetId = 12345,
linkInterceptionCallbacks = linkCallbacks,
modifier = Modifier.fillMaxWidth()
)
}

Example 5: Widget with Controller

Programmatic control with reload and navigation:

main.kt
@Composable
fun ControlledWidget() {
val controller = rememberStoriesViewController()
val widgetProperties by controller.widgetProperties.collectAsState()

Column {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(8.dp)
) {
Button(onClick = { controller.load() }) {
Text("Reload")
}
Button(onClick = {
controller.openWidgetStoryByPosition(0) { result ->
Log.d("Widget", "Result: $result")
}
}) {
Text("Open First")
}
}

widgetProperties?.let {
Text(
"Size: ${it.storyWidth}x${it.storyHeight}",
modifier = Modifier.padding(8.dp),
style = MaterialTheme.typography.bodySmall
)
}

StoriesView(
widgetId = 12345,
controller = controller,
modifier = Modifier.fillMaxWidth()
)
}
}

Example 6: Pull-to-Refresh Widget

Swipe down to refresh stories:

main.kt
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RefreshableWidgetExample() {
val controller = rememberStoriesViewController()
var isRefreshing by remember { mutableStateOf(false) }

PullToRefreshBox(
isRefreshing = isRefreshing,
onRefresh = {
isRefreshing = true
controller.load()
}
) {
StoriesView(
widgetId = 12345,
controller = controller,
loadEventCallbacks = StorifyMeLoadEventCallbacks(
onLoad = { _, _ -> isRefreshing = false },
onFail = { _ -> isRefreshing = false }
),
modifier = Modifier.fillMaxWidth()
)
}
}

Best Practices

1. Always Use remember* Helper Functions

Use the provided helper functions to ensure configuration survives recomposition:

main.kt
// Good - Config is remembered
val config = rememberStoriesViewConfig(
segments = listOf("premium")
)

// Bad - Config is recreated on every recomposition
val config = StorifyMeWidgetConfig.Builder()
.setSegments(listOf("premium"))
.build()

2. Keep Widget IDs Stable

Widget IDs should be stable across recompositions:

main.kt
// Good - Stable widget ID
@Composable
fun MyWidget() {
StoriesView(widgetId = 12345)
}

// Careful - Widget recreates when state changes
@Composable
fun DynamicWidget(count: Int) {
StoriesView(widgetId = count.toLong()) // Recreates on every count change
}

3. Leverage Automatic Lifecycle Management

The widget automatically handles lifecycle - no manual cleanup needed:

main.kt
// Correct - Automatic lifecycle management
@Composable
fun MyScreen() {
StoriesView(widgetId = 12345)
} // Widget disposed automatically when leaving composition

4. Use Stable Callbacks

Create stable callback references to avoid unnecessary recomposition:

main.kt
// Good - Stable callback
@Composable
fun MyWidget() {
val onLoad = remember {
{ widgetId: Long, stories: List<StoryWithSeen> ->
Log.d("Widget", "Loaded $widgetId")
}
}

StoriesView(widgetId = 12345, onLoad = onLoad)
}

// Bad - New callback on every recomposition
@Composable
fun MyWidget() {
StoriesView(
widgetId = 12345,
onLoad = { widgetId, stories ->
Log.d("Widget", "Loaded $widgetId")
}
)
}

5. Use Modifiers for Layout Control

Let Compose modifiers handle layout and styling:

main.kt
StoriesView(
widgetId = 12345,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)

6. Enable Height Caching for Lists

When using widgets in LazyColumn, enable height caching:

main.kt
LazyColumn {
items(10) { index ->
StoriesView(
widgetId = (100L + index),
enableHeightCache = true, // Prevents scroll jumping
modifier = Modifier.fillMaxWidth()
)
}
}

7. Handle Errors Gracefully

Always provide error handling:

main.kt
StoriesView(
widgetId = 12345,
loadEventCallbacks = StorifyMeLoadEventCallbacks(
onFail = { error ->
Log.e("Widget", "Load failed: ${error.message}")
// Show error UI or retry
}
)
)

Migration from View-Based Integration

Comparison: XML View vs Compose

AspectXML ViewCompose
IntegrationAdd to XML layoutUse composable function
ConfigurationBuilder pattern (StorifyMeWidgetConfig.Builder())Helper functions (rememberStoriesViewConfig())
Event ListenersGlobal StorifyMe.instance.eventListenerPer-widget callbacks
LifecycleManual (in Activity/Fragment)Automatic (Compose lifecycle)
UpdatesImperative (storiesView.load())Declarative recomposition

Migration Steps

1. XML Layout to Composable

Before (XML):

<com.storify.android_sdk.ui.view.StoriesView
android:id="@+id/storiesView"
android:layout_width="match_parent"
android:layout_height="150dp" />
// Activity
val storiesView = findViewById<StoriesView>(R.id.storiesView)
storiesView.widgetId = 12345
storiesView.load()

After (Compose):

@Composable
fun MyScreen() {
StoriesView(
widgetId = 12345,
modifier = Modifier.fillMaxWidth()
)
}

2. Configuration Builder to Helper Functions

Before (View):

val config = StorifyMeWidgetConfig.Builder().apply {
setLanguage("en")
setDirection(StorifyMeContentDirection.LTR)
setSegments(listOf("premium", "vip"))
}.build()

storiesView.config = config

After (Compose):

val config = rememberStoriesViewConfig(
language = "en",
direction = StorifyMeContentDirection.LTR,
segments = listOf("premium", "vip")
)

StoriesView(widgetId = 12345, config = config)

3. Global Event Listener to Per-Widget Callbacks

Before (View):

// Global listener affects all widgets
StorifyMe.instance.eventListener = object : StorifyMeEventListener() {
override fun onLoad(widgetId: Long, stories: List<StoryWithSeen>) {
Log.d("Widget", "Loaded")
}

override fun onStoryOpened(story: StoryWithSeen, index: Int) {
Log.d("Widget", "Story opened")
}
}

After (Compose):

// Per-widget callbacks
StoriesView(
widgetId = 12345,
loadEventCallbacks = StorifyMeLoadEventCallbacks(
onLoad = { widgetId, stories ->
Log.d("Widget", "Loaded")
}
),
lifecycleCallbacks = StorifyMeLifecycleCallbacks(
onStoryOpened = { story, index ->
Log.d("Widget", "Story opened")
}
)
)

4. Imperative Operations to Controller

Before (View):

val storiesView = findViewById<StoriesView>(R.id.storiesView)

// Reload
storiesView.load()

// Open story
storiesView.openWidgetStoryByPosition(0) { result ->
// Handle result
}

After (Compose):

val controller = rememberStoriesViewController()

// Reload
controller.load()

// Open story
controller.openWidgetStoryByPosition(0) { result ->
// Handle result
}

StoriesView(widgetId = 12345, controller = controller)

Complete Migration Example

Before (Activity with XML):

class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val storiesView = findViewById<StoriesView>(R.id.storiesView)

val config = StorifyMeWidgetConfig.Builder().apply {
setLanguage("en")
setSegments(listOf("premium"))
}.build()

storiesView.config = config
storiesView.widgetId = 12345
storiesView.load()

StorifyMe.instance.eventListener = object : StorifyMeEventListener() {
override fun onLoad(widgetId: Long, stories: List<StoryWithSeen>) {
Toast.makeText(this@MainActivity, "Loaded ${stories.size} stories", Toast.LENGTH_SHORT).show()
}

override fun onStoryOpened(story: StoryWithSeen, index: Int) {
Log.d("Stories", "Opened: ${story.story.name}")
}
}
}
}

After (Compose):

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

setContent {
MyAppTheme {
StoriesScreen()
}
}
}
}

@Composable
fun StoriesScreen() {
val scope = rememberCoroutineScope()
val snackbarHostState = remember { SnackbarHostState() }

val config = rememberStoriesViewConfig(
language = "en",
segments = listOf("premium")
)

Scaffold(snackbarHost = { SnackbarHost(snackbarHostState) }) { padding ->
StoriesView(
widgetId = 12345,
config = config,
loadEventCallbacks = StorifyMeLoadEventCallbacks(
onLoad = { _, stories ->
scope.launch {
snackbarHostState.showSnackbar("Loaded ${stories.size} stories")
}
}
),
lifecycleCallbacks = StorifyMeLifecycleCallbacks(
onStoryOpened = { story, _ ->
Log.d("Stories", "Opened: ${story.story.name}")
}
),
modifier = Modifier
.fillMaxWidth()
.padding(padding)
)
}
}

Key Differences Summary

  1. Declarative vs Imperative: Compose uses a declarative approach where you describe what the UI should look like
  2. Lifecycle: Compose handles lifecycle automatically - no need for onDestroy cleanup
  3. Per-Widget Callbacks: Each widget can have its own callbacks instead of a global listener
  4. Type Safety: remember* helpers provide better type safety and prevent common mistakes
  5. State Management: Compose's state system makes it easier to update UI based on data changes

Summary

The StorifyMe Jetpack Compose integration provides a modern, declarative API for integrating stories into your Compose applications. Key features include:

  • Native Compose Support: First-class composable functions
  • Automatic Lifecycle: No manual cleanup required
  • Type-Safe Configuration: Use remember* helpers for stable configuration
  • Per-Widget Callbacks: Fine-grained event handling
  • Advanced Features: Controller, personalization, pull-to-refresh, height caching
  • Easy Migration: Clear path from View-based to Compose

For additional help, refer to the View-based documentation or the Configuration and Events guides.