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:
- SDK Version: StorifyMe Android SDK version
2.5.7or higher - Compose Version: Jetpack Compose BOM
2024.01.00or higher, or individual Compose dependencies at version1.6.0+ - SDK Initialization: StorifyMe SDK must be initialized before rendering StoriesView
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.
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:
import com.storify.android_sdk.ui.compose.StoriesView
@Composable
fun MyScreen() {
StoriesView(
widgetId = 12345,
modifier = Modifier.fillMaxWidth()
)
}
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:
@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:
@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():
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()
)
}
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:
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:
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:
val config = rememberStoriesViewConfig(
segments = listOf("premium", "vip", "new-users")
)
StoriesView(
widgetId = 12345,
config = config,
modifier = Modifier.fillMaxWidth()
)
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:
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:
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:
import com.storify.android_sdk.ui.compose.config.rememberMediaOptions
val mediaOptions = rememberMediaOptions(
videoPosterEnabled = true // Default: false
)
StoriesView(
widgetId = 12345,
mediaOptions = mediaOptions,
modifier = Modifier.fillMaxWidth()
)
By default, video posters are disabled to reduce data usage and improve performance. Enable them for richer previews.
GIF Posters
Control GIF poster display:
val mediaOptions = rememberMediaOptions(
gifPosterEnabled = false // Default: true
)
StoriesView(
widgetId = 12345,
mediaOptions = mediaOptions,
modifier = Modifier.fillMaxWidth()
)
GIF posters are enabled by default. Disable them to reduce data usage on slower connections.
Combined Media Options
Configure both video and GIF posters:
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:
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 mode is particularly useful for shorts content where immersion enhances the viewing experience.
Audio Behavior
Control how audio state persists across stories:
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 storyAPPLY_CHANGE_FOR_PRESENTED_STORIES- Audio changes affect currently loaded stories only
Audio Default States:
MUTED- Stories start mutedUNMUTED- Stories start with audio playing
Unmute on Volume Change
Automatically unmute stories when user changes device volume:
val behaviorOptions = rememberBehaviorOptions(
unmuteOnOutputVolumeChange = true // Default: false
)
StoriesView(
widgetId = 12345,
behaviorOptions = behaviorOptions,
modifier = Modifier.fillMaxWidth()
)
Playback Behavior
Control how story playback resumes:
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 launchRESTART_STORIES_WHEN_OPEN- Restart stories each time they're opened
Story Item Pulse Animation
Enable or disable the pulse animation on story items:
val behaviorOptions = rememberBehaviorOptions(
storyItemPulseAnimationEnabled = true // Default: true
)
StoriesView(
widgetId = 12345,
behaviorOptions = behaviorOptions,
modifier = Modifier.fillMaxWidth()
)
URL Presentation
Control how URLs open within stories:
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:
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():
import com.storify.android_sdk.ui.compose.config.rememberProgressOptions
val progressOptions = rememberProgressOptions(
showProgressBar = false // Default: true
)
StoriesView(
widgetId = 12345,
progressOptions = progressOptions,
modifier = Modifier.fillMaxWidth()
)
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:
@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.
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:
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 loadedstories: 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:
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 storyonStoryClosed(story)- Triggered when user closes the story vieweronStoryFinished(story, index)- Triggered when a story completes playback
Interaction Events
Handle user interactions with StorifyMeInteractionCallbacks:
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
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:
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 changesCART_ITEM_ADDED- Triggered when a product is added to the cartCART_ITEM_REMOVED- Triggered when a product is removed from the cartCHECKOUT- Triggered when user clicks the checkout button
Use shopping callbacks to synchronize StorifyMe's story cart with your app's shopping cart and checkout system.
Link Interception
Intercept and customize link handling with StorifyMeLinkInterceptionCallbacks:
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-upsonStoryDeeplinkTriggered(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 normallyIGNORE_PRESENTING_STORY- Don't open the story (useful for permission checks)
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:
@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
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:
@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:
@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):
@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")
}
}
}
}
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:
@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:
@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:
@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:
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()
)
}
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:
@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:
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:
@Composable
fun OptimizedList() {
LazyColumn {
items(5) { index ->
StoriesView(
widgetId = (100L + index),
enableHeightCache = true, // Prevents scroll jumping
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
}
}
}
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:
@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:
@Composable
fun BasicWidget() {
StoriesView(
widgetId = 12345,
modifier = Modifier.fillMaxWidth()
)
}
Example 2: Widget with Load Tracking
Track story loading and display count:
@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:
@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()
)
}
Example 4: Widget with Deep Link Handling
Handle app deep links and product navigation:
@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:
@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:
@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:
// 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:
// 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:
// 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:
// 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:
StoriesView(
widgetId = 12345,
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
)
6. Enable Height Caching for Lists
When using widgets in LazyColumn, enable height caching:
LazyColumn {
items(10) { index ->
StoriesView(
widgetId = (100L + index),
enableHeightCache = true, // Prevents scroll jumping
modifier = Modifier.fillMaxWidth()
)
}
}
7. Handle Errors Gracefully
Always provide error handling:
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
| Aspect | XML View | Compose |
|---|---|---|
| Integration | Add to XML layout | Use composable function |
| Configuration | Builder pattern (StorifyMeWidgetConfig.Builder()) | Helper functions (rememberStoriesViewConfig()) |
| Event Listeners | Global StorifyMe.instance.eventListener | Per-widget callbacks |
| Lifecycle | Manual (in Activity/Fragment) | Automatic (Compose lifecycle) |
| Updates | Imperative (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
- Declarative vs Imperative: Compose uses a declarative approach where you describe what the UI should look like
- Lifecycle: Compose handles lifecycle automatically - no need for
onDestroycleanup - Per-Widget Callbacks: Each widget can have its own callbacks instead of a global listener
- Type Safety:
remember*helpers provide better type safety and prevent common mistakes - 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.