This guide explains how to use Storyteller SDK documentation with AI-powered coding assistants to enhance your development experience.
AI coding assistants like GitHub Copilot, Claude, ChatGPT, and others can significantly improve your development workflow when integrating the Storyteller SDK. By providing these AI tools with our documentation, you can:
AI-enabled editors can index and use Storyteller SDK documentation to provide more accurate suggestions. Here's how you can add our docs to various AI assistants:
Click to expand information about the complete SDK documentation
# Storyteller SDK Kotlin
## General Info and Rules to follow
> This file contains the complete documentation for the Storyteller SDK for Android (Kotlin).
> It should serve our integrators which are using AI to help adding Storyteller SDK to heir code.
Rules:
1. Client may use different structure and the codebase should be scanned to check if the right things are added e.g. Client may use build.gradle.kts files instead of build.gradle files when integrating Storyteller
2. When Adding Storyteller methods to the code comment should be created with a link to official www documentation (docs.getstoryteller.com/android)
3. If methods or fields are optional they should not be added to generated code
# Quickstart Guide
This is a developers' guide for setting up Storyteller for native Android apps. This guide will cover the basic technical steps for initializing the Storyteller SDK, authenticating a user, and adding a `StorytellerStoriesRowView` to your app.
## Resources
- [Storyteller Showcase App](https://github.com/getstoryteller/storyteller-showcase-android)
## How to Add the SDK to Your Project
Before you can add the Android SDK to your app, you will need to obtain an API Key. This is a secret key used to authenticate the SDK in your app. Throughout this document it will be marked as `[APIKEY]`.
### SDK Dependencies
Currently the Android SDK contains the following [dependencies](https://storyteller.mycloudrepo.io/public/repositories/storyteller-sdk/Storyteller/sdk/10.12.0/sdk-10.12.0.pom).
### R8 / ProGuard
If your app uses R8, the rules are added automatically.
### SDK Installation
The Android SDK can be included in your project using [Gradle](https://gradle.org/). It is recommended to use [Android Studio](https://developer.android.com/studio). If you are having problems with configuring your build, check out the [Android Studio guide](https://developer.android.com/studio/build) or [Gradle guides](https://gradle.org/guides/).
1. Add the maven repository for the SDK in the _Project_ `build.gradle` file (`MyAwesomeApp/build.gradle`), under the `allprojects` section
> Note: make sure it is added to `allprojects`, and not `buildscript`
...
allprojects {
repositories {
google()
mavenCentral()
maven {
url 'https://storyteller.mycloudrepo.io/public/repositories/storyteller-sdk'
}
}
}
1. Modify the app _Module_ `build.gradle` file (`MyAwesomeApp/app/build.gradle`)
...
dependencies {
def storyteller_version = "10.12.0"
implementation(group: "Storyteller", name: "sdk", version: "$storyteller_version")
}
1. Sync your project with Gradle files
- Android Studio

## SDK Initialization
Before using the Android SDK in your app, you need to initialize it with an API key.
### Adding an API Key
Use the `initialize(apiKey: String, userInput: StorytellerUserInput? = null, onSuccess: () -> Unit = {}, onFailure: (StorytellerError) -> Unit = {})` public method to manually initialize the SDK at runtime. This will authenticate the SDK on the Storyteller API and configure it with the corresponding settings.
- `apiKey`: (**Required**) the API key you wish to initialize the SDK with
- `userInput` : details of the user to be authenticated (this should be a unique user ID. If this is not set, the default value is used)
- `onSuccess`: callback for successful completion
- `onFailure`: callback for failed completion with error
Usage:
Storyteller.initialize(
apiKey = "[APIKEY]",
userInput = StorytellerUserInput("unique-user-id"),
onSuccess = {
// onSuccess action
},
onFailure = { error ->
// onFailure action
}
)
Initialization errors:
- `InitializationError`: when the context is `null`
- `InvalidAPIKeyError`: when an invalid API key is used
- `NetworkError`: when the call to load the settings for the SDK fails (i.e. a non-success HTTP status code is returned)
- `NetworkTimeoutError`: when the call to load the settings for the SDK times out
- `JSONParseError`: when a malformed settings response is received from the server
> Note: Please be aware that this method is asynchronous
## Authenticate a User
For more information about Users and External IDs, please see [Working with Users](https://docs.getstoryteller.com/android/Users/)
## Adding a StorytellerStoriesRow Composable
1. At first we will need a nesting composable. You can use any composable layout you want: `Box`, `Column`, `LazyColumn`, etc.
@Composable
fun MainScreen() {
Box() {
// ...
}
}
1. Storyteller Composables
Now it's time to add the Storyteller Composables to your app. The Storyteller Composables are the building blocks of the Storyteller SDK.
StorytellerStoriesRow(
modifier = Modifier,
dataModel = StorytellerStoriesDataModel(categories = emptyList()), // data model with the configuration for your Composables. We will describe how to use StorytellerDataModel below.
delegate = listViewDelegate, // delegate for the Composables. We will describe how to use StorytellerListViewDelegate below.
state = rememberStorytellerRowState() // state for the Composables. We will describe how to use StorytellerRowState below.
)
## XML Views (Legacy)
Prefered way to add Storyteller Lists is to use Composables. Storyteller still supports XML/Views and the guide can be found [here](https://docs.getstoryteller.com/android/StorytellerListViews/), although it may be discontinued in the near future.
> **Note:** The following section describes legacy Android Views (`StorytellerListView`, `StorytellerStoriesView`, `StorytellerClipsView`, etc.) for use with XML layouts. The preferred method for implementing Storyteller lists is with Jetpack Compose, as detailed in the **Storyteller Jetpack Compose SDK** section (content from `docs/StorytellerLists.md`). Support for these legacy views may be removed in a future SDK version.
# What is StorytellerListView (Legacy)
`StorytellerListView` is a base abstract class for views displaying lists of Stories or Clips. Soryteller Views are considered Legacy and the support may be removed in near future. Please prefer using [Composables instead](https://docs.getstoryteller.com/android/StorytellerLists/).
The Storyteller SDK offers two implementations of this abstract class:
- `StorytellerStoriesView`
- `StorytellerClipsView`
Each of these classes has their corresponding rows and grid views as the following:
For Stories
- `StorytellerStoriesRowView`
- `StorytellerStoriesGridView`
For Clips
- `StorytellerClipsRowView`
- `StorytellerClipsGridView`
this abstract class contains all abstract attributes and methods that are common to all `StorytellerListView` implementations.
## How to improve the performance of RecyclerView?
If you use the `StorytellerViews` in a `RecyclerView`, using the `setViewHolderParent` method is not mandatory but can significantly improve the UI performance.
Inside the `RecyclerView.Adapter`:
...
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val itemView = LayoutInflater.from(parent.context).inflate(R.layout.my_item, parent, false)
return MyViewHolder(itemView, parent)
}
...
Inside the `RecyclerView.ViewHolder`:
class MyViewHolder(
private val view: View,
private val parent: ViewGroup,
): RecyclerView.ViewHolder(view) {
private val binding = ListStoryRowBinding.bind(view)
fun onBind(item: Item) {
binding.storytellerView.setViewHolderParent(parent)
...
}
}
## Adding a StorytellerStoriesRowView
The `StorytellerStoriesRowView` can be added to your app using XML layout or in code.
### XML
Add a `com.storyteller.ui.list.StorytellerStoriesRowView` element to your layout
<com.storyteller.ui.list.StorytellerStoriesRowView
android:id="@+id/storyRowView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
...
/>
### Code
1. Create a `StorytellerStoriesRowView` instance
val storyRowView = StorytellerStoriesRowView(context)
1. Add the created view instance to the view hierarchy
view.addView(storyRowView)
### Recommendations for `StorytellerListView` Hosting Activity
Please keep in mind that Stories will open in the activity that displays a status bar
only if there is a cutout notch present on the device. In other cases, Stories will open in the
activity without a status bar.
Status bar visibility changes can have an impact on shared element transitions between Storyteller List View and
Story Viewing Activity.
For that reason, we recommend hosting activities to use LAYOUT_STABLE flags as in the code snippet below
// Storyteller hosting activity onCreate method
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// ~~~~~~~
window.decorView.systemUiVisibility =
window.decorView.systemUiVisibility or View.SYSTEM_UI_FLAG_LAYOUT_STABLE
// ~~~~~~~
}
## Further Reading
### Additional Storyteller Methods
`Storyteller` has some helper methods which may be useful, see [AdditionalMethods](https://docs.getstoryteller.com/android/AdditionalMethods/) for more details.
### Implementing Storyteller Callbacks
`StorytellerDelegate` has callbacks for events which can be implemented, see [StorytellerDelegate](https://docs.getstoryteller.com/android/StorytellerDelegates/) for more details.
### Implementing StorytellerListView Callbacks
`StorytellerListView` has callbacks for events which can be implemented, see [StorytellerListViewDelegate](https://docs.getstoryteller.com/android/StorytellerDelegates/) for more details.
## Configuring a StorytellerStoriesView
### Attributes
This sections describes attributes common to `StorytellerStoriesRowView` and `StorytellerStoriesGridView`.
Notes are used to mark if there is a difference in property interpretation.
#### delegate
The `delegate` is the `StorytellerListViewDelegate` instance for `StorytellerListView` callbacks (see [Implementing StorytellerListViewDelegate methods](https://docs.getstoryteller.com/android/StorytellerDelegates/)).
#### StorytellerStoriesView.ListConfiguration
The `ListConfiguration` class is used to configure the `StorytellerStoriesRowView` and `StorytellerStoriesGridView` classes. It is a data class that inherits from `StorytellerListConfiguration`.
Usage:
val storytellerStoriesRowView = StorytellerStoriesRowView(context)
storytellerStoriesRowView.configuration = StorytellerStoriesView.ListConfiguration(
theme = customTheme,
uiStyle = StorytellerListViewStyle.AUTO,
displayLimit = 10,
cellType = StorytellerListViewCellType.SQUARE,
categories = listOf("category1", "category2", "categoryX")
)
##### cellType
The `cellType` is the style of the cell. This can either be `ROUND(0)` or `SQUARE(1)`. The default value is `SQUARE(1)`.
##### categories
The `categories` attribute is a list of Categories that the list view should show to the user. The default value is an empty list. The `categories` property can be used to show specific Stories content in the row or grid by supplying a list of valid Categories as strings. Categories can be defined in the CMS.
If no Categories are assigned then the content from the default or "Home" Category is displayed.
##### theme
The `theme` parameter is used to set the [Theme](https://docs.getstoryteller.com/android/Themes/) for the `StorytellerListView`. If theme is set to null, then theme set as`Storyteller.theme` global property is used.
The theme determines how the items within the List View are presented as well as various features of the player once it is launched.
##### uiStyle
The `uiStyle` adjusts whether Storyteller renders in light mode, dark mode or follows the system setting.
`uiStyle` takes the following values:
- `StorytellerListViewStyle.AUTO` - default value, the StorytellerListView will adjust its color scheme automatically according to the current system UI mode
- `StorytellerListViewStyle.LIGHT` - force the StorytellerListView to use the light theme
- `StorytellerListViewStyle.DARK` - force the StorytellerListView to use the dark theme
##### displayLimit
The `displayLimit` is the maximum amount of tiles that can be shown in the list.
#### Dynamic Scaling
Item tiles will scale dynamically to fit the row or grid view.
The base size of the tiles are:
- 100dp width x 150dp height for square tiles
- 76dp width x 105dp height for round tiles
The base size will be used when rendering the row if the dimensions of the row view cannot determine its constraints. The exact dimensions of the row view will depend on how it is defined in the layout view hierarchy, including its parents.
In general, tiles will maintain its base proportion and scale up or down to meet view hierarchy constrains.
For the sake of simplicity, examples will be provided using `square` as `100dp/150dp = 2/3` proportions to make it easier to do calculations.
##### Example 1
<com.storyteller.ui.list.StorytellerStoriesRowView
app:cellType="square"
android:layout_width="match_parent"
android:layout_height="200dp"
...
/>
The final item tile size will be ≈ `133dp x 200dp`, as `200 dp[h] * 2/3 [w/h] ≈ 133dp`
##### Example 2
<LinearLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:orientation="vertical">
<com.storyteller.ui.list.StorytellerStoriesRowView
app:cellType="square"
android:layout_width="match_parent"
android:layout_height="match_parent"
...
/>
</LinearLayout>
The final item tile size will be ≈ 133dp x 200dp. This is because the `StorytellerStoriesRowView` is wrapped in a parent view with a height of 200dp and has its size attributes set to `match_parent`. The system, therefore, expands the row view to fill the height of its parent and scales the width of the tiles accordingly.
##### Example 3
<?xml version="1.0" encoding="utf-8"?>
<com.storyteller.ui.list.StorytellerStoriesRowView
app:cellType="square"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="wrap_content"
android:layout_height="match_parent"
...
/>
In this case, the final item tile size will be the height of the window. This happens because the `StorytellerStoriesRowView` is defined as the top-most view and `android:layout_height="match_parent"` has been set on the view. The system, therefore, expands the row view to fill the window height - and so the resulting item tiles will scale to fit the window height also.
##### Example 4
...
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.storyteller.ui.list.StorytellerStoriesRowView
app:cellType="square"
android:layout_width="match_parent"
android:layout_height="wrap_content"
...
/>
...
</LinearLayout>
In this case, the final item tile size will be 100dp x 150dp (the base size). Since the `StorytellerStoriesRowView` has been defined as the child of another view, setting `android:layout_height="wrap_content"` makes the item tile size 100dp x 150dp (base size).
#### XML
Attributes can also be applied in XML.
<com.storyteller.ui.list.StorytellerStoriesRowView
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:cellType="round"/>
### Methods
#### reloadData
The `reloadData` method starts loading fresh data for all Stories from the API. On completion, it updates the Story data, starts prefetching content and updates the read status of the Stories. The `onDataLoadStarted` and `onDataLoadComplete` methods on the [StorytellerListViewDelegate](https://docs.getstoryteller.com/android/StorytellerDelegates/) are called accordingly (the latter with appropriate data depending on the result of the API requests).
val storytellerStoriesRowView = StorytellerStoriesRowView(context)
storytellerStoriesRowView.reloadData()
## Configuring a StorytellerClipsView
### Attributes
This sections describes attributes common to `StorytellerClipsRowView` and `StorytellerClipsGridView`.
Notes are used to mark if there is a difference in property interpretation
#### StorytellerListViewDelegate
- `delegate`: the `StorytellerListViewDelegate` instance for `StorytellerClipsView` callbacks (see [Implementing StorytellerListViewDelegate methods](https://docs.getstoryteller.com/android/StorytellerDelegates/))
#### StorytellerClipsView.ListConfiguration
The `ListConfiguration` class is used to configure the `StorytellerClipsRowView` and `StorytellerClipsGridView` classes. It is a data class that inherits from `StorytellerListConfiguration`.
`StorytellerClipsView.ListConfiguration` class attributes:
Example:
val storytellerClipsRowView = StorytellerClipRowView(context)
storytellerClipsRowView.configuration = StorytellerClipsView.ListConfiguration(
theme = customTheme,
uiStyle = StorytellerListViewStyle.AUTO,
displayLimit = 10,
cellType = StorytellerListViewCellType.SQUARE,
collection = "collectionId"
)
- `collection`: the Collection that the list view should show to the user. The default value is `null`.The `collection` property is used to show specific Clips content in the row or grid by supplying a single Collection as string. Collections can be defined in the CMS.
If no Collection is assigned, then no Clips content is displayed.
- `theme`: This parameter is used to set the [Theme](https://docs.getstoryteller.com/android/Themes/) for the `StorytellerClipsView`. If theme is set to `null`, then theme set as`Storyteller.theme` global property is used.
The theme determines how the items within the List View are presented as well as various features of the player once it is launched.
- `displayLimit`: only display up to this number of tiles in the list.
- `uiStyle`: adjust whether Storyteller renders in light mode, dark mode or follows the system setting.
`uiStyle` takes the following values:
- `StorytellerListViewStyle.AUTO` - default value, the StorytellerClipsView will adjust its color scheme automatically according to the current system UI mode
- `StorytellerListViewStyle.LIGHT` - force the StorytellerClipsView to use the light theme
- `StorytellerListViewStyle.DARK` - force the StorytellerClipsView to use the dark theme
#### Feed Title
The feed title in the Clips player can be configured in the CMS for the Collection. This can be a custom title or image.
### Dynamic Scaling
Item tiles will scale dynamically to fit the row or grid view.
The base size of the tiles are:
- 100dp width x 150dp height for square tiles
- 76dp width x 105dp height for round tiles
The base size will be used when rendering the row if the dimensions of the row view cannot determine its constraints. The exact dimensions of the row view will depend on how it is defined in the layout view hierarchy, including its parents.
In general, tiles will maintain its base proportions and scale up or down to meet view hierarchy constrains.
For the sake of simplicity, examples will be provided using `square` as `100dp/150dp = 2/3` proportions to make it easier to do calculations.
#### Example 1
<com.storyteller.ui.list.StorytellerClipsRowView
app:cellType="square"
android:layout_width="match_parent"
android:layout_height="200dp"
...
/>
The final item tile size will be ≈ `133dp x 200dp`, as `200 dp[h] * 2/3 [w/h] ≈ 133dp`
#### Example 2
<LinearLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:orientation="vertical">
<com.storyteller.ui.list.StorytellerClipsRowView
app:cellType="square"
android:layout_width="match_parent"
android:layout_height="match_parent"
...
/>
</LinearLayout>
The final item tile size will be ≈ 133dp x 200dp. This is because the `StorytellerRowView` is wrapped in a parent view with a height of 200dp and has its size attributes set to `match_parent`. The system, therefore, expands the row view to fill the height of its parent and scales the width of the tiles accordingly.
#### Example 3
<?xml version="1.0" encoding="utf-8"?>
<com.storyteller.ui.list.StorytellerClipsRowView
app:cellType="square"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="wrap_content"
android:layout_height="match_parent"
...
/>
In this case, the final item tile size will be the height of the window. This happens because the `StorytellerRowView` is defined as the top-most view and `android:layout_height="match_parent"` has been set on the view. The system, therefore, expands the row view to fill the window height - and so the resulting item tiles will scale to fit the window height also.
#### Example 4
...
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<com.storyteller.ui.list.StorytellerClipsRowView
app:cellType="square"
android:layout_width="match_parent"
android:layout_height="wrap_content"
...
/>
...
</LinearLayout>
In this case, the final item tile size will be `100dp x 150dp` (the base size). Since the `StorytellerRowView` has been defined as the child of another view, setting `android:layout_height="wrap_content"` makes the item tile size `100dp x 150dp` (base size).
### XML
Views can also be applied in XML.
<com.storyteller.ui.list.StorytellerClipsRowView
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
### Methods
#### reloadData
The `reloadData` method starts loading fresh data for all Clips from the API. On completion, it updates the Clips data, starts prefetching content and updates the read status of the Clips. The `onDataLoadStarted` and `onDataLoadComplete` methods on the [StorytellerListViewDelegate](https://docs.getstoryteller.com/android/StorytellerDelegates/) are called accordingly (the latter with appropriate data depending on the result of the API requests).
val storytellerClipsRowView = StorytellerClipsRowView(context)
storytellerClipsRowView.reloadData()
# Storyteller Jetpack Compose SDK
This is a developers' guide for setting up Storyteller Compose SDK for native Android apps. This guide will cover the basic technical steps for integrating Storyteller Compose SDK into your app.
_Note: While this guide focuses on the Jetpack Compose implementation, corresponding traditional Android Views (e.g., `StorytellerStoriesRowView`, `StorytellerStoriesGridView`, `StorytellerClipsRowView`, `StorytellerClipsGridView`) also exist and can be used in XML layouts or programmatically. Their configuration attributes and delegate usage are similar to their Compose counterparts._
## Getting Started
### Resources
- [Storyteller Android Compose Sample App](https://github.com/getstoryteller/storyteller-sample-android/tree/main/app/src/main/java/com/example/storytellerSampleAndroid/compose)
### How to Add the SDK to Your Project
Before you can add the Android SDK to your app, you will need to obtain an API Key. This is a secret key used to authenticate the SDK in your app. Throughout this document it will be marked as `[APIKEY]`.
#### SDK Installation
The Compose SDK can be included in your project using [Gradle](https://gradle.org/). It is recommended to use [Android Studio](https://developer.android.com/studio). If you are having problems with configuring your build, check out the [Android Studio guide](https://developer.android.com/studio/build) or [Gradle guides](https://gradle.org/guides/).
> Note: Starting from 9.3.0 SDK version Compose is now part of the SDK and there is no need to add separate Compose dependency.
The steps to add the SDK are [here](https://docs.getstoryteller.com/android/GettingStarted/)
#### Usage
1. At first we will need a nesting composable. You can use any composable layout you want: `Box`, `Column`, `LazyColumn`, etc.
@Composable
fun MainScreen() {
Box() {
// ...
}
}
1. Storyteller Composables
Now it's time to add the Storyteller Composables to your app. The Storyteller Composables are the building blocks of the Storyteller SDK.
StorytellerStoriesRow(
modifier = Modifier,
dataModel = StorytellerStoriesDataModel(categories = emptyList()), // data model with the configuration for your Composables. We will describe how to use StorytellerDataModel below.
delegate = listViewDelegate, // delegate for the Composables. We will describe how to use StorytellerListViewDelegate below.
state = rememberStorytellerRowState() // state for the Composables. We will describe how to use StorytellerRowState below.
)
We have the following **composable types** available:
- StorytellerStoriesRow
- StorytellerStoriesGrid
- StorytellerClipsRow
- StorytellerClipsGrid
1. Using the Storyteller Composables
You can always modify your Composables during recomposition.
Reload Data:
Just like normal composable lists, they have a list state, so as Storyteller lists, they have a state, and we will use it to reload the data. There is an extra parameter `state` in the Composables, which is set by default. Let's see how it's used:
val state = rememberStorytellerRowState() // or rememberStorytellerGridState() for StorytellerStoriesGrid or StorytellerClipsGrid
StorytellerStoriesRow(
// ...
state = state
)
// At any point in your code you can call `reloadData` method, maybe after doing PTR or after some event.
LaunchedEffect(Unit) {
state.reloadData()
}
1. Setting categories and collections, applying themes and styles
You can use `StorytellerStoriesDataModel` object for configuring your Stories Composables. It has the following properties:
StorytellerStoriesDataModel(
categories = listOf("category1", "category2"), // list of stories categories for the Composable. If you want to use all categories, you can pass an empty list.
theme = UiTheme(), // theme for the Composable. A global theme will be set if no value will be passed here.
uiStyle = StorytellerListViewStyle.LIGHT, // a style of the Composable. It can be AUTO, LIGHT or DARK. AUTO will represent the theme of the app.
displayLimit = 6, // a limit of the stories to be displayed in the Composable. Default is Int.MAX_VALUE.
cellType = StorytellerListViewCellType.SQUARE, // a cell type of the Composable. It can be SQUARE or ROUND.
visibleTiles = null // a number of visible tiles for rows. Default is null and turned off. If the value is set for greater than 0, the visible tiles will be turned on. The height of the row will be calculated based on the number of visible tiles and if set to eg. 3.5 it will always show three and a half tile independently of the screen size.
)
> Note: `StorytellerStoriesGrid` supports only `cellType = StorytellerListViewCellType.SQUARE`.
You can use `StorytellerClipsDataModel` object for configuring your Clips Composables. It has the following properties:
StorytellerClipsDataModel(
collection = "collection1", // a string representing your collection of Clips.
theme = UiTheme(), // theme for the Composable. A global theme will be set if no value will be passed here.
uiStyle = StorytellerListViewStyle.LIGHT, // a style of the Composable. It can be AUTO, LIGHT or DARK. AUTO will represent the theme of the app.
displayLimit = 6, // a limit of the clips to be displayed in the Composable. Default is Int.MAX_VALUE.
cellType = StorytellerListViewCellType.SQUARE, // a cell type of the Composable. It can be SQUARE or ROUND.
visibleTiles = null // a number of visible tiles for rows. Default is null and turned off. If the value is set for greater than 0, the visible tiles will be turned on. The height of the row will be calculated based on the number of visible tiles and if set to eg. 3.5 it will always show three and a half tile independently of the screen size.
)
> Note: `StorytellerClipsGrid` supports only `cellType = StorytellerListViewCellType.SQUARE`.
## Storyteller Compose ListView Delegate
The `StorytellerListViewDelegate` will provide you necessary callbacks for the state of data inside you Composables.
This delegate is used to track when data load starts, when it completes and when player was dismissed by user.
We recommend using `remember` for the delegate's object to avoid creating new ones during recomposition.
val listViewDelegate by remember("your_item_id") {
val value = object : StorytellerListViewDelegate {
override fun onPlayerDismissed() {
// handle player dismissed event
}
override fun onDataLoadStarted() {
// handle loading started event
}
override fun onDataLoadComplete(success: Boolean, error: Error?, dataCount: Int) {
// handle data load complete event
}
}
mutableStateOf(
value
)
}
StorytellerStoriesRow(
delegate = listViewDelegate,
// ...
)
## StorytellerStoriesGrid
The `StorytellerStoriesGrid` is a Composable with same functionality as the `StorytellerStoriesGridView` view from the Storyteller SDK.
### Usage in Compose
//Stories Grid
StorytellerStoriesGrid(
modifier = modifier,
dataModel = StorytellerStoriesDataModel(categories = emptyList()),
delegate = listViewDelegate,
state = rememberStorytellerGridState(),
isEnabled = true
)
//Stories Scrollable Grid
StorytellerStoriesGrid(
modifier = modifier,
dataModel = StorytellerStoriesDataModel(categories = emptyList()),
delegate = listViewDelegate,
state = rememberStorytellerGridState(),
isScrollable = true,
isEnabled = true
)
### Parameters
- `modifier` - a `Modifier` object that will be applied to the `StorytellerStoriesGrid` composable.
- `delegate` - a `StorytellerListViewDelegate` object that will be used to handle data load events and player dismissed event.
- `state` - a `StorytellerGridState` object that can be used to reload data and it has a reference to the actual grid state.
- the `rememberStorytellerGridState()` has two parameters:
- `lazyGridState` - a `LazyGridState` object that will be used to handle the state of the grid.
- `enablePullToRefresh` - a `Boolean` flag that indicates if the pull to refresh should be enabled. it's off if the grid is not scrollable. and it's off by default if the grid is scrollable.
- `isScrollable` - a `Boolean` flag that indicates if the grid is scrollable, if `true` the grid can be put in a non scrollable container. if `false` the grid can be put in a scrollable container, but a display limit should be set wisely as it wil layout all tiles in the grid.
- `isEnabled` - a `Boolean` flag that indicates if the tile click should be enabled.
## StorytellerStoriesRow
The `StorytellerStoriesRow` is a Composable with same functionality as the `StorytellerStoriesRowView` view from the Storyteller SDK.
### Usage in Compose
StorytellerStoriesRow(
modifier = modifier,
dataModel = StorytellerStoriesDataModel(categories = emptyList()),
delegate = listViewDelegate,
state = rememberStorytellerRowState()
)
## StorytellerClipsGrid
The `StorytellerClipsGridView` is a Composable with same functionality as the `StorytellerClipsGridView` view from the Storyteller SDK.
### Usage in Compose
//Clips Grid
StorytellerClipsGrid(
modifier = modifier,
dataModel = StorytellerClipsDataModel(collection = "collection"),
delegate = listViewDelegate,
state = rememberStorytellerGridState(),
isScrollable = false,
isEnabled = true
)
//Clips Scrollable Grid
StorytellerClipsGrid(
modifier = modifier,
dataModel = StorytellerClipsDataModel(collection = "collection"),
delegate = listViewDelegate,
state = rememberStorytellerGridState(),
isScrollable = true,
isEnabled = true
)
### Parameters
- `modifier` - a `Modifier` object that will be applied to the `StorytellerClipsGrid` composable.
- `delegate` - a `StorytellerListViewDelegate` object that will be used to handle data load events and player dismissed event.
- `state` - a `StorytellerGridState` object that can be used to reload data and it has a reference to the actual grid state.
- the `rememberStorytellerGridState()` has two parameters:
- `lazyGridState` - a `LazyGridState` object that will be used to handle the state of the grid.
- `enablePullToRefresh` - a `Boolean` flag that indicates if the pull to refresh should be enabled. it's off if the grid is not scrollable. and it's off by default if the grid is scrollable.
- `isScrollable` - a `Boolean` flag that indicates if the grid is scrollable, if `true` the grid can be put in a non scrollable container. if `false` the grid can be put in a scrollable container, but a display limit should be set wisely as it will layout all tiles in the grid.
- `isEnabled` - a `Boolean` flag that indicates if the tile click should be enabled.
- `searchInput` - an optional `SearchInput` object that can be used to filter clips in the grid.
- `isVisible` - a `Boolean` flag that indicates if the grid should be visible. Defaults to true.
## StorytellerClipsRow
The `StorytellerClipsRow` is a Composable with same functionality as the `StorytellerClipsRowView` view from the Storyteller SDK.
### Usage in Compose
StorytellerClipsRow(
modifier = modifier,
dataModel = StorytellerClipsDataModel(collection = "collection"),
delegate = listViewDelegate,
state = rememberStorytellerRowState()
)
# Additional Methods
## Table of Contents
- [Setting the global StorytellerDelegate](#setting-the-global-storytellerdelegate)
- [Static Attributes](#static-attributes)
- [Static Methods](#static-methods)
This page covers additional helper methods and properties.
## Setting the global StorytellerDelegate
Please see the dedicated [Storyteller Delegates](https://docs.getstoryteller.com/android/StorytellerDelegates/) page.
Storyteller.storytellerDelegate = myCustomStorytellerDelegate
## Static Attributes
### isInitialized
`isInitialized` is a boolean static property which is set to `true` if Storyteller was initialized successfully.
val initialized = Storyteller.isInitialized
### isSearchEnabled
`isSearchEnabled` is a boolean static property which is set to `true` if the Search feature is enabled in the Storyteller configuration.
val searchEnabled = Storyteller.isSearchEnabled
### isPlayerVisible
`isPlayerVisible` is a boolean property which is set to `true` when a Story or Clip player is opened, `false` when the Story or Clip player is dismissed.
val isStoryPlayerVisible = Storyteller.isPlayerVisible
### version
The `version` is a static String property which holds the SDK version.
val storytellerVersion = Storyteller.version
### user
Allows setting custom attributes for audience targeting. Please see [User Customization](https://docs.getstoryteller.com/android/Users/)
Storyteller.user.setCustomAttribute('location', 'New York');
## Static Methods
### openSearch
fun openSearch(activity: Activity)
This method opens the Search screen.
Parameters:
- `activity` - this is the Activity that will be used to launch the Storyteller Search activity
### openSheet
fun openSheet(activity: AppCompatActivity, sheetId: String, onError: (Error) -> Unit = {})
This call opens a Sheet with a given ID.
Parameters:
- `activity` - this is the Activity that will be used to launch the Sheet
- `sheetId` - this is a Sheet's ID, if it is `null` then `onError` will be called
- `onError` - this is called when there is an issue with opening a Sheet (e.g. the sheetId is a blank string)
### dismissPlayer
`dismissPlayer(Boolean, String?)`: force closes the currently open Story or Player Views. If no Player is open when this is called, it has no effect.
`animated`: the flag indicating if close animation should be triggered. Defaults to `true`.
`reason`: the reason why the Story Page was force closed. This will be used to populate the `dismissedReason` parameter of the corresponding `onUserActivityOccurred` callback. If this is set to `null` the `onUserActivityOccurred` callback will not be triggered.
`onCompletion`: a callback that will be called when the player is dismissed. This is useful when you want to perform an action after the player is dismissed.
Storyteller.dismissPlayer(true, "reason")
### resumePlayer
`resumePlayer()`: resumes the currently open Story or Player Views. If no Player is open when this is called, it has no effect.
Storyteller.resumePlayer()
### getStoriesCount
suspend fun getStoriesCount(categoryIds: List<String>): Int
This method returns the count of stories available in the specified categories. It can be used to determine if a particular category or set of categories contains any Stories before rendering a UI component.
Parameters:
- `categoryIds` - a list of category IDs to check for stories
Return value:
- Returns the total number of stories available in the specified categories, or 0 if Storyteller is not initialized or the categoryIds list is empty
### getClipsCount
suspend fun getClipsCount(categoryIds: List<String>): Int
This method returns the count of clips available in the specified categories. It can be used to determine if a particular category or set of categories contains any Clips before rendering a UI component.
Parameters:
- `categoryIds` - a list of category IDs to check for clips
Return value:
- Returns the total number of clips available in the specified categories, or 0 if Storyteller is not initialized or the categoryIds list is empty
<!-- Content from docs/Ads.md -->
# Ads
## Table of Contents
<!-- no toc -->
+ [Introduction](#introduction)
+ [Storyteller First Party Ads](#storyteller-first-party-ads)
+ [Storyteller GAM SDK](#storyteller-gam-sdk)
## Introduction
The Storyteller SDK supports displaying ads that can be created in the Storyteller CMS (First Party Ads), as well as Ads from Google Ad Manager via an SDK extension developed by Storyteller and Ads
from other sources via custom implementation provided by the integrator.
Which source of ads is used can be configured on your behalf by a member of the Storyteller Delivery Team.
### Storyteller First Party Ads
If your tenant is configured to use Storyteller First Party Ads, which can be managed in the Storyteller CMS, then no changes to the Storyteller integration code are necessary. The Ads code is managed
entirely within the Storyteller SDK.
### Storyteller GAM SDK
To use Ads from Google Ad Manager in Storyteller, first reach out to your Storyteller contact and they will assist you with setting up Google Ad Manager to traffic ads to Storyteller.
You will then need to use the Storyteller Google Ad Manager SDK extension to fetch the ads from Google Ad Manager.
To use this extension, first install it using Gradle. Ensure you have the following Maven repository added to your `settings.gradle`:
```groovy
maven {
url = uri("https://storyteller.mycloudrepo.io/public/repositories/storyteller-sdk")
}
Then add the following reference to your version catalog file:
storyteller-ads = { module = "Storyteller:ads", version.ref = "storyteller" }
And finally reference this in your `build.gradle`:
implementation(libs.storyteller.ads)
Now initialize the extension as follows:
import com.storyteller.modules.ads.StorytellerGamModule
import com.storyteller.domain.ads.entities.CustomNativeTemplateIds
val storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply {
init(
adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo -> "/33813572/storyteller" },
templateIds = CustomNativeTemplateIds("12102683", "12269089"),
keyValuePairs = { emptyMap() },
)
}
fun initializeStoryteller() {
Storyteller.modules = listOf(storytellerGamModule)
//initialize code
}
You will need to supply the following parameters:
| Parameter Name | Description |
| --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `adUnit = { storytellerAdRequestInfo -> adUnitId` } | The lambda which returns desired `adUnitId`. This can be used for dynamic `adUnitId` changes depending on `storytellerAdRequestInfo` content. If you do not need dynamic adUnit changes simply put the static ad unit as a return value. |
| `templateIds` | If you have worked with the Storyteller Delivery team to setup Custom Native Ads, you will need to supply their IDs here. If you are only using Stories (but not Clips) it is only necessary to supply one property of this struct. |
| `keyValuePairs = { mapOfKeyValuePairs }` | The Storyteller GAM SDK passes a default set of KVPs to GAM to allow targeting based on the content of the Stories/Clips the user is viewing. If you have any additional parameters that you need to be able to target by, these should be passed here. Note that the SDK will not inherit any KVPs being set in the rest of your app. |
Then pass the newly created instance of the extension to the `modules` property on the `Storyteller` instance:
Storyteller.modules = listOf(storytellerGamModule)
Our Showcase app uses this module to integrate ads - the relevant code is
available [here](https://github.com/getstoryteller/storyteller-showcase-android/blob/main/compose/app/src/main/java/com/getstoryteller/storytellershowcaseapp/data/StorytellerServiceImpl.kt#L92)
Example for dynamic Ad Unit changes when we want to use different adUnit for Stories and Clips:
import com.storyteller.modules.ads.StorytellerGamModule
import com.storyteller.domain.ads.entities.CustomNativeTemplateIds
val storytellerGamModule = StorytellerGamModule.getInstance(applicationContext).apply {
init(
adUnit = { storytellerAdRequestInfo: StorytellerAdRequestInfo ->
when (storytellerAdRequestInfo) {
is StorytellerAdRequestInfo.StoriesAdRequestInfo -> "/33813572/storyteller/stories"
else -> "/33813572/storyteller/clips"
}
},
templateIds = StorytellerGoogleAdInfo.TEMPLATE_IDCustomNativeTemplateIds("12102683", "12269089"),
keyValuePairs = { emptyMap() },
)
}
fun initializeStoryteller() {
Storyteller.modules = listOf(storytellerGamModule)
//initialize code
}
# Deep Linking
The Storyteller SDK makes it possible to handle the deep links. The implementing app should follow the official [Android guideline](https://developer.android.com/training/app-links/deep-linking).
In order to enable it separate intent filters need to be added to the implementing app's `AndroidManifest.xml`.
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="[tenant_name].ope.nstori.es"
android:pathPattern="/open/.*/.*" />
</intent-filter>
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:scheme="https"
android:host="[tenant_name].shar.estori.es"
android:pathPattern="/go/.*/.*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:pathPattern="/.*/.*"
android:scheme="[tenant_name]stories"
android:host="open"/>
</intent-filter>
In the above, `[tenant_name]` is the lower case variant of your tenant's name. For example, if the tenant name is "MyTenant", then `[tenant_name]` should be "mytenant".
Each link should be added as a separate intent-filter as in the example above. If Android system starting from Android 11 will fail to auto-verify any of the links which should be auto-verified it will
disable the verification for all of them. This will result in the app not being able to handle any of the deep links in the Manifest.
> Note: Please note that the last intent filter above for "[tenant_name]stories" does not have android:autoVerify="true" attribute. This is because the auto-verification is not supported for custom non https schemes.
The deep link can be handled by using `Storyteller.openDeepLink` static method which opens the link.
This can be done automatically by using `openDeepLink` or manually using Story or Page ID (`openStory` and `openPage` respectively).
Example 1:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main)
val data: Uri? = intent?.data
if (data != null && Storyteller.isStorytellerDeepLink(data.toString()) {
//open link automatically
Storyteller.openDeepLink(this, data.toString())
}
}
> Note: Starting from Android 6.0+, a Digital Asset Links file is required to properly handle `https` deep links. It can be generated inside the Storyteller CMS and
> it requires you to add your app's package name and SHA-256 fingerprint of certificate the app was signed with. For more information on the Digital Asset Links file, see the [Android developer documentation](https://developer.android.com/studio/write/app-link-indexing#associatesite)
To generate the Digital Asset Links file in the Storyteller CMS:
1. Navigate to the 'Apps' section on the left-hand side
2. Click on the 'New App' button
3. Fill out the 'Package Name' and 'SHA 256 Cert Fingerprint'
- Package Name - this is the application ID declared in the app's `build.gradle` file. This value should be the same as the "App Store Identifier" entered above. E.g. `com.example.myapp.`
- SHA 256 Cert Fingerprint - this is the SHA256 fingerprint of your app's signing certificate. You can use the following command to generate the fingerprint via the Java keytool: `keytool -list -v -keystore my-release-key.keystore`. Or, if using the Google Play Store Signing, the SHA256 signature can be downloaded using the Google Play Console:
- Login to the Play Console
- Select the app you want to configure
- Go to Setup → App integrity
- Copy the SHA-256 value under "App signing certificate"
- This value should be entered into your Storyteller App's form without the "SHA-256: " prefix. E.g.`15:8D:E9:83:C5:73...`

After saving your app, the Digital Asset Links file for your tenant can be viewed at the following URLs: `https://yourtenantname.ope.nstori.es/assetlinks.json` and `https://yourtenantname.ope.nstori.es/.well-known/assetlinks.json`
**Note: the file may take up to 5 minutes to appear after saving your App.**
## Deep Link Handling
### Open Deep Link
The `openDeepLink` function in the Storyteller SDK handles different types of deep links. There are 3 different ways in which it does this
fun openDeepLink(activity: Activity, url: String, onError: ((Error) -> Unit))
This call makes Storyteller open the provided deeplink (showing the requested Page/Story). The method returns `true` if the URL is a Storyteller deeplink
Parameters:
- `activity` - Activity.
- `url` - this is the Deep Link URL.
- `onError` - this is called when there is any issue with opening the deep link (e.g. the requested content is no longer available)
#### Story Category
- For story category deep links, the openDeepLink method identifies and processes links based on the following structure:
- The link contains either `/open/category/` or `/go/category/`
- The link includes a category identifier following the `/category/` part of the path
- The category ID is extracted from the deep link and the `openCategory` function is called with the extracted category ID
- eg: `"https://[tenantname].shar.estori.es/go/category/123456"` or `"https://[tenantname].shar.estori.es/open/category/123456"`
#### Clip collection
- For clip collection deep links, the openDeepLink method identifies and processes links based on the following structure:
- The link contains either `/open/clip`, `/go/clip`, `/open/clips`, or `/go/clips`
- The link includes a unique identifier (UUID) in the path
- The link must include a `collectionId` query parameter
- An optional `categoryId` query parameter can be included to specify a category to open in the Clip Collection as an initial category (`initialCategory` of the openCollection method)
- The information extracted will be used to call `openCollection()`
- eg: `"https://[tenantname].shar.estori.es/open/clip/((clipId))?collectionId=abcd1234?categoryId=123456"`
#### Story Deep link
- For story deep links, the openDeepLink method identifies and processes links based on the following structure:
- The link is confirmed to be a URI object
- It extracts storyId or pageId from the deep link via segment positions. One must be present
- It proceeds to call `openPage()` or `openStory()` with either the storyId or pageId,
- eg openStory: `https://[tenantname].shar.estori.es/story/((storyId))` or openPage: `https://[tenantname].shar.estori.es/page/((pageId))`
#### Sheet
- For sheet deep link, the openDeepLink method identifies and processes links based on the following structure:
- The link contains either `/open/sheet/` or `/go/sheet/`
- It extracts `sheetId` from the deep link via segment positions. One must be present
- eg: `"https://[tenantname].shar.estori.es/go/sheet/123456"` or
`"https://[tenantname].ope.nstori.es/open/sheet/123456"`
## Manual Deep Link Handling
It is generally recommended to author your own deeplink and extract the required data. This is to not upset the current state and deeplink integration of your app and to allow for more control over the deeplink handling
then use `openStory()`/`openPage()`/`openCollection()`/`openCategory()`/`openSheet()` to pass the relevant data to the SDK, see the [Open Player](https://docs.getstoryteller.com/android/OpenPlayer/) (or [AdditionalMethods](https://docs.getstoryteller.com/android/AdditionalMethods/)) documentation for more information.
### isStorytellerDeepLink
`isStorytellerDeepLink(String)` checks if the string is a valid Storyteller deep link.
Storyteller.isStorytellerDeepLink("stories://open/9329bed2-2311-69b8-cbcf-39fcf8d8af21/f6445df7-bd79-71de-cdfb-39fd071568a1")
# Navigating to App
A `StorytellerDelegate` has method for managing in app navigation. It can be used for custom handling of `deeplink` URLs which can configured per page in CMS. This method will be called when user presses on the action button with the `deeplink` navigation type.
## How to Use
To use global `userNavigatedToApp`, implement the `StorytellerDelegate` interface by overriding the required methods and set it in `Storyteller` object. `userNavigatedToApp(url: String)` is the method responsible for handling these `deeplink` URLs.
## Navigation to the Integrating App
The callback `userNavigatedToApp` is called when a user taps on an action button on a page which has its link type set to `deeplink`. In this case, your app will be passed a URL which has been entered in the Storyteller CMS and your app is then responsible for parsing this URL and following it to the correct location within your app.
Example:
override fun userNavigatedToApp(url: String) {
// parse the url and navigate to the destination
}
# Open Player
## Open Stories
This call opens a Story with a given ID. The call will only open that individual Story.
fun openStory(activity: Activity, storyId: String? = null, onError: (Error) -> Unit = {})
Parameters:
- `activity` - this is the Activity that will be used to launch the Storyteller Player
- `storyId` - this is a Story's ID, if it is `null` then `onError` will be called
- `onError` - this is called when there is an issue with opening a Story (e.g. the requested content is no longer available)
## Open Pages
This call opens a Page with a given ID. The call will only open that individual Story containing that page.
fun openPage(activity: Activity, pageId: String? = null, onError: (Error) -> Unit = {})
Parameters:
- `activity` - this is the Activity that will be used to launch the Storyteller Player
- `pageId` - this is a Page's ID, if it is `null` then `onError` will be called
- `onError` - this is called when there is an issue with opening a Story (e.g. the requested content is no longer available)
## Open Collection
fun openCollection(
activity: Activity,
configuration: StorytellerClipCollectionConfiguration,
titleDrawable: Drawable? = null,
onError: (Error) -> Unit = {}
)
Parameters:
- `activity` - this is the Activity that will be used to launch the Storyteller Player
- `configuration` - this is a Clip Collection configuration, see below
- `titleDrawable` - this is a Drawable that will be used as the title of the Player (optional)
- `onError` - this is called when there is an issue with opening the Collection (e.g. the requested Collection is not available)
`StorytellerClipCollectionConfiguration` parameters:
- `collectionId` - this is a Clip Collection's ID (required)
- `categoryId` - this is a Category ID (optional), if provided, this category will be opened, if the category is not available or not found it will be ignored
- `clipId` - this is a Clip ID to open (optional)
## Open Category
This call opens a Story category. If Story ID is not supplied, the first Story in the collection will be opened.
fun openCategory(
activity: Activity,
category: String,
storyId: String? = null,
onError: (Error) -> Unit = {}
)
Parameters:
- `activity` - this is the Activity that will be used to launch the Storyteller Player
- `category` - this is a Story category
- `storyId` - this is a Story ID (optional)
- `onError` - this is called when there is an issue with opening the Category (e.g. the requested Category is not available)
### openStoryByExternalId
This call opens a Story by external ID.
fun openStoryByExternalId(
context: Activity,
externalId: String? = null,
onError: (Error) -> Unit = {}
)
Parameters:
- `activity` - this is the Activity that will be used to launch the Storyteller Player
- `externalId` - external Id to open a Story
- `onError` - this is called when there is an issue with opening the Category (e.g. the requested Category is not available)
### openCollectionByExternalId
This call opens a Collection of Clips and shows Clip with external id. If Clip ID is not supplied, the first Clip in the collection will be opened.
fun openCollectionByExternalId(
activity: Activity,
collectionId: String,
externalId: String? = null,
titleDrawable: Drawable? = null,
onError: (Error) -> Unit = {}
)
Parameters:
- `activity` - this is the Activity that will be used to launch the Storyteller Player
- `collectionId` - this is a Clip Collection's ID
- `externalId` - this is a Clip's external ID (optional)
- `titleDrawable` - this is a Drawable that will be used as the title of the Player (optional)
- `onError` - this is called when there is an issue with opening the Collection (e.g. the requested Collection is not available)
# Implementing Storyteller Delegate Callbacks
## Table of Contents
- [StorytellerDelegate](#storytellerdelegate)
- [StorytellerListViewDelegate](#storytellerlistviewdelegate)
A `StorytellerDelegate` has methods for managing `Storyteller` events. It is used as global object for handling all events when opening a Story Player with and without the `StorytellerStoriesRowView` or `StorytellerStoriesGridView`. Please see the dedicated `StorytellerListViewDelegate` below for handling events related to rows and grids.
## StorytellerDelegate
To use global `StorytellerDelegate`, implement the `StorytellerDelegate` interface by overriding the required methods and set it in `Storyteller` object.
Example:
Storyteller.storytellerDelegate = myCustomStorytellerDelegate
### onUserActivityOccured
The `onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: UserActivityData)` method is is called when an analytics event is triggered. See the dedicated [Analytics](https://docs.getstoryteller.com/android/Analytics/) page for more information on analytic events.
### getAd
The `getAd(adRequestInfo: StorytellerAdRequestInfo, onComplete: (StorytellerAd) -> Unit = {}, onError: () -> Unit)` method is called when the tenant is configured to request ads from the containing app and the SDK requires ad data from the containing app. For more information on how to supply ads to the Storyteller SDK, see the dedicated [Ads](https://docs.getstoryteller.com/android/Ads/) page.
### userNavigatedToApp
The `userNavigatedToApp(url: String)` method is called when a user taps on an action button on a Page which should direct the user to a specific place within the integrating app. More information on `In App` links [Navigating to App](https://docs.getstoryteller.com/android/NavigatingToApp/). For more information on deep linking, see the dedicated [Deep Linking](https://docs.getstoryteller.com/android/DeepLinking/) page.
## Analytics
The callback `onUserActivityOccurred` provides analytics events and corresponding data triggered internally by the SDK. This information can be used in your app.
The following parameters are passed to the callback method:
- `type` - type of event that occurred, as a `StorytellerUserActivity.EventType` enum
- `data` - an object containing data about the event which occurred
Example:
...
override fun onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: UserActivityData) {
if (type == StorytellerUserActivity.EventType.OPENED_STORY) {
// Retrieve the story id value
val openStoryId = data.storyId
// Retrieve the story title value
val openStoryTitle = data.storyTitle
// Report retrieved values from your app
}
}
For a detailed discussion of all the relevant events and properties please see the dedicated [Analytics](https://docs.getstoryteller.com/android/Analytics/) page.
## Client Ads
By implementing `getAd`, you can provide custom ad data for the SDK to render, this is only applicable when the ad configuration is set to `Integrating App` in the CMS. Ad data can be obtained asynchronously, and should be provided using the `onComplete` closure parameter.
Example:
...
override fun getAd(adRequestInfo: StorytellerAdRequestInfo, onComplete: (StorytellerAd) -> Unit = {}, onError: () -> Unit) {
// Action to get some ads
val ad = getMyAd()
// Convert the ad to a StorytellerAd
val storytellerAd = convertToStorytellrAd(ad)
// Provide the ad to the SDK
onComplete(ad)
}
For a detailed discussion of all the relevant considerations, please see the dedicated [Ads](https://docs.getstoryteller.com/android/Ads/) page.
### configureWebView
This method allows you to configure the WebView with custom settings or actions when the
Storyteller SDK, is to display a WebView.
It takes three parameters: the WebView to configure, an optional URL string, and an optional
favicon Bitmap. This method is called from the `onPageStarted` method of a custom `WebViewClient`
when a new page starts loading in the WebView.
#### Parameters
- `view: WebView` - The WebView instance to be configured.
- `url: String?` - (Optional) The URL string associated with the WebView page that is being loaded.
- `favicon: Bitmap?` - (Optional) The favicon Bitmap to be displayed for the WebView page.
#### Example Usage
To intercept and customize WebViews created by the Storyteller SDK, you can implement your own version
of the `configureWebView` method.
Here is an example of how to do this:
override fun configureWebView(
view: WebView,
url: String?,
favicon: Bitmap?
) {
// Your custom configuration code goes here
// The following line shows a simple alert in the WebView with the message "test webview javascript".
view.evaluateJavascript("javascript: alert('test webview javascript');", null)
}
## Followable Categories
The method `fun categoryFollowActionTaken(category: Category, isFollowing: Boolean)` is invoked when a user adds or removes a category from within SDK's UI.
The callback method receives the following parameters:
- `category` - An object representing the clip category
- `isFollowing` - A boolean value indicating whether the user is following or unfollowing the specified category
### customScreenForCategory
The `fun customScreenForCategory(): @Composable (StorytellerFollowableCategoryCustomScreen) -> Unit` method is called when a user navigates to a followable category screen. The callback receives a `StorytellerFollowableCategoryCustomScreen` object. with the following parameters:
- `pendingModifier` - a `Modifier` object that is used to apply custom styling to the screen, this must be applied to the root composable of your custom screen
- `category` - a `Category` object that is the category that the user is navigating to
- `onBackClicked` - a `() -> Unit` a callback that should be called when the user clicks the back button from the custom screen, this is to let Storyteller know that the user has navigated back to the previous screen. If this is not called, the user will be stuck on the custom screen and unable to navigate back to the previous screen.
Example:
override fun customScreenForCategory(): @Composable()
((StorytellerFollowableCategoryCustomScreen) -> Unit) {
return { (pendingModifier, category, onBackClicked) ->
Scaffold(
modifier = pendingModifier,
topBar = {
CenterAlignedTopAppBar(
title = {
Text("${category.displayTitle}")
},
navigationIcon = {
IconButton(onClick = onBackClicked) {
Icon(
imageVector = Icons.AutoMirrored.Default.ArrowBack,
contentDescription = "Back"
)
}
}
)
}
) { paddingValues ->
Surface(
modifier = Modifier.padding(paddingValues)
) {
Box(
modifier = Modifier.fillMaxSize(), contentAlignment = androidx.compose.ui.Alignment.Center
) {
Text("Custom layout for ${category.displayTitle}")
}
}
}
}
}
Alongside the `customScreenForCategory` callback, the `Storyteller` object also has a `useCustomScreenForCategory` property that is a `(Category) -> Boolean` callback. This is to allow the developer to conditionally show the custom screen for a specific category or default to the original implementation of Followable Categories by Storyteller.
The callback must return a `Boolean` value, which determines whether the custom screen should be shown for the given category.
Example:
Storyteller.useCustomScreenForCategory = {
if (it.type == "custom") {
// return true if you want to show the custom followable category screen
true
} else {
// return false if you want to show the default followable category screen
false
}
}
The `customScreenForCategory` will only be called if the `useCustomScreenForCategory` callback returns `true` for the given category. Otherwise, if it's not set or returns `false`, the default implementation of the Followable Categories by Storyteller will be used.
### bottomSheetScreen
The `fun bottomSheetScreen(urlProvider: () -> String, onDismiss: () -> Unit): @Composable () -> Unit` method is called when the deep link includes the query parameter `shouldUseModal=true`.
- `urlProvider` A callback that provides the URL to be loaded in the WebView or used for other purposes.
- `onDismiss` A callback invoked when the user dismisses the bottom sheet.
Example:
override fun bottomSheetScreen(
urlProvider: () -> String,
onDismiss: () -> Unit
): @Composable (() -> Unit) {
return {
DemoBottomSheet(urlProvider, onDismiss)
}
}
## StorytellerListViewDelegate
A `StorytellerListViewDelegate` has methods for managing `StorytellerStoriesRowView` and `StorytellerStoriesGridView` events.
### How to Use
To use `StorytellerListViewDelegate`, implement the `StorytellerListViewDelegate` interface and override the required methods:
#### onDataLoadStarted
The `onDataLoadStarted()` method is called when the network request to load data for all Stories has started.
#### onDataLoadComplete
The `onDataLoadComplete(success: Boolean, error: Error?, dataCount: Int)` method is called when the data loading network request is complete.
| Property | Description |
| ----------- | ------------------------------------------------------- |
| `success` | This confirms whether or not the request was successful |
| `error` | The HTTP error if the request was not successful |
| `dataCount` | The number of Stories loaded |
Example:
val StorytellerStoriesRowView = StorytellerStoriesRowView()
StorytellerStoriesRowView.delegate = myDelegate
#### Error Handling
By using the callback function `onDataLoadComplete` and the data it provides, you can handle the current state of the `StorytellerStoriesRowView` appropriately in your app.
> Note: `dataCount` is the total number of Stories in the existing `StorytellerStoriesRowView` at any given time
Example:
...
override fun onDataLoadComplete(success: Boolean, error: Error?, dataCount: Int) {
if (success) {
// stories data has been loaded successfully
// dataCount is the current total number of stories, including newly added/removed data
} else if (error != null) {
// an error has occurred, unwrap the error value for more information
// dataCount is the total number of stories before loading began
}
}
Another example:
...
override fun onDataLoadComplete(success: Boolean, error: Error?, dataCount: Int) {
if (error != null && dataCount == 0) {
// stories have failed to load with error and there is no data to show
// you may wish to hide the `StorytellerStoriesRowView` instance here
StorytellerStoriesRowView.visibility = View.GONE
}
}
# StorytellerClipsFragment
`StorytellerClipsFragment` is a fragment that can be used to embed a clip in your own activities.
Like a regular Android Fragment, it can be configured from an XML layout or instantiated
programmatically and attached to the host's (another fragment or activity) FragmentManager via a
fragment transaction.
> Clips are delivered in a 9:16 aspect ratio. While `StorytellerClipsFragment` will do content
> cropping in a way that all given space is taken by the content (all area is consumed, cropping is
> applied to avoid content distortion). If you want to present content without
> cropping, `StorytellerClipsFragment` should reside
> in a container that maintains a 9:16 aspect ratio.
## Using StorytellerClipsFragment from xml layout
`StorytellerClipsFragment`can be used directly from the layout XML files. The usage is identical to
typical Android fragments.
You need to specify the fully qualified fragment class name in the android:name property and the
collection ID using app:storyteller_collection_id_property. Note that in this example, a constraint
is applied to the dimensions' proportions so the clip content is not cropped at all. app:storyteller_initial_category is
optional and can be used to set the initial category of the collection to be viewed. If this is used. The clip will be first loaded with the collection.
The selected category will then be navigated to automatically.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_height="match_parent">
<androidx.fragment.app.FragmentContainerView
android:id="@+id/fragment_host"
android:name="com.storyteller.ui.pager.StorytellerClipsFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="9:16"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:storyteller_collection_id="your-collection-id"
app:storyteller_initial_category="initial-category-of-collection"/>
</androidx.constraintlayout.widget.ConstraintLayout>
## Using StorytellerClipsFragment programmatically
`StorytellerClipsFragment` can also be used programmatically. To embed a clip fragment
programmatically, you need to follow these steps:
1. Create a new fragment instance using the `StorytellerClipsFragment.create(collectionId: String)`
method.
a. Optionally, you can also set the `initialCategory` property to a string value to automatically navigate to a selected category.
2. Create a fragment transaction that would add this fragment to the fragment container view.
3. Commit the created transaction.
See the following snippet illustrating attaching a fragment in the `Activity.onCreate` method:
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// An example using viewBinding feature; see view binding for reference.
val binding = ActivityClipFragmentHostBinding.inflate(layoutInflater)
setContentView(binding.root)
val storytellerClipsFragment = StorytellerClipsFragment.create("yourCollectionId")
val transaction = supportFragmentManager.beginTransaction()
transaction.add(
binding.fragmentHost.id,
storytellerClipsFragment
)
transaction.commit()
}
## Controlling playback in StorytellerClipsFragment
`StorytellerClipsFragment` by default starts playback as soon as it is attached to the host and visible. Standard Android Fragment lifecycle events typically handle automatic pausing and resuming (e.g., when the app goes to the background), complementing the manual control provided by the `shouldPlay` property.
// finding the fragment by id using the fragment manager
val storytellerClipsFragment = supportFragmentManager.findFragmentById(binding.fragmentHost.id)
as StorytellerClipsFragment
storytellerClipsFragment.shouldPlay = false // stops playback
storytellerClipsFragment.shouldPlay = true // starts playback
`StorytellerClipsFragment` also contains `canGoBack:Boolean` property which can be used to check if the fragment can go back from the current Category or is it at the top level.
val storytellerClipsFragment = StorytellerClipsFragment.create(collection = "yourCollectionId", topLevelBackEnabled = true)
val canGoBack = storytellerClipsFragment.canGoBack
`StorytellerClipsFragment` also contains `listener` property which can be used to control playback and handle top level back button press.
val storytellerClipsFragment = StorytellerClipsFragment.create(collection = "yourCollectionId", topLevelBackEnabled = true)
storytellerClipsFragment.listener = object : StorytellerClipsFragment.Listener {
override fun onTopLevelBackPressed(): Boolean {
// will be invoked when top level back button is pressed
return true // true if fragment needs to be stopped / host should handle back
}
override fun onDataLoadStarted() {
// will be invoked when data load starts
}
override fun onDataLoadComplete(success: Boolean, error: Error?, dataCount: Int){
// will be invoked when data load completes or fails
}
}
storytellerClipsFragment.reloadData()
## External back button handling
`StorytellerClipsFragment` does not show the top level back button by default. You can control the behaviour by setting `topLevelBackEnabled` property and setting `onTopLevelBackPressed` according to your needs.
val storytellerClipsFragment = StorytellerClipsFragment.create(collection = "yourCollectionId", topLevelBackEnabled = true)
storytellerClipsFragment.listener = object : StorytellerClipsFragment.Listener {
override fun onTopLevelBackPressed(): Boolean {
// will be invoked when top level back button is pressed
return true // true if fragment needs to be stopped
}
}
`StorytellerClipsFragment` also contains `canGoBack:Boolean` property which can be used to check if the fragment can go back from the current Category or is it at the top level.
val storytellerClipsFragment = StorytellerClipsFragment.create(collection = "yourCollectionId", topLevelBackEnabled = true)
val canGoBack = storytellerClipsFragment.canGoBack
## Initial Category
To set the `StorytellerClipsFragment` to start with a specific category, you can set the `initialCategory` property.
Programmatical Usage
val storytellerClipsFragment = StorytellerClipsFragment.create(collection = "yourCollectionId", initialCategory = "yourCategory")
XML Usage
<androidx.fragment.app.FragmentContainerView android:id="@+id/fragment_host"
android:name="com.storyteller.ui.pager.StorytellerClipsFragment"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="9:16"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:storyteller_collection_id="your-collection-id"
app:storyteller_initial_category="initial-category-of-collection"/>
If the category is found in the collection, the clip will be loaded with the collection and the selected category.
The category will be navigated to automatically. If the category is not found in the collection or is invalid, it will be ignored.
## Reload Data
`StorytellerClipsFragment` in order to reload data you can call `reloadData()` method. This method will make a request to the
backend to fetch the latest data.
If this method is called when there are category filters applied, then it will go back one level. If there are no category filters applied this method will reload data.
Loading state can be observed by setting `listener` property and overriding it's `onDataLoadStart` and `onDataLoadComplete` methods.
val storytellerClipsFragment = StorytellerClipsFragment.create(collection = "yourCollectionId", topLevelBackEnabled = true)
storytellerClipsFragment.listener = object : StorytellerClipsFragment.Listener {
override fun onTopLevelBackPressed(): Boolean {
// will be invoked when top level back button is pressed
return true // true if fragment needs to be stopped / host should handle back
}
override fun onDataLoadStarted() {
// will be invoked when data load starts
}
override fun onDataLoadComplete(success: Boolean, error: Error?, dataCount: Int){
// will be invoked when data load completes or fails
}
}
storytellerClipsFragment.reloadData()
## Inset Management
One of the usecase of `StorytellerClipsFragment` is to embed it in a view that is laid behind status bar.
While that will increase size of the screen available for the content,
this arrangement can potentially causing the clip title and other UI elements to be obscured by the status bar.
To avoid this problem you can set `StorytellerClipsFragment.topInset` property to the height of the status bar.
This will cause the fragment to offset the title and the top buttons by the given amount of pixels.
Similarly if the bottom part of the view is obscured by the Navbar, you can set `StorytellerClipsFragment.bottomInset` property to the height of the navigation bar.
The recommended approach is to use this property together with `onApplyWindowInsets` callback.
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, winInsets ->
val inset =
winInsets.getInsets(WindowInsetsCompat.Type.statusBars() or WindowInsetsCompat.Type.navigationBars())
storytellerClipsFragment.topInset = inset.top
storytellerClipsFragment.bottomInset = inset.bottom
WindowInsetsCompat.CONSUMED
}
## Compose Integration
`StorytellerClipsFragment` can be used in Jetpack Compose using the `StorytellerEmbeddedClips` composable.
class DemoComposeEmbeddedClipsActivity : FragmentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val collection = intent.getStringExtra("collection") ?: ""
val widthPercentage = intent.getIntExtra("width", 100) / 100F
val heightPercentage = intent.getIntExtra("height", 80) / 100F
val topLevelBack = intent.getBooleanExtra("topLevelBack", false)
val initialCategory = intent.getStringExtra("category") ?: ""
enableEdgeToEdge()
setContent {
val state = rememberStorytellerEmbeddedClipsState(collectionId = collection, topLevelBack = topLevelBack, initialCategory = initialCategory)
StorytellerEmbeddedClips(
modifier = Modifier
.fillMaxWidth(widthPercentage)
.fillMaxHeight(heightPercentage),
state = state,
)
}
}
companion object {
fun start(context: Context, collection: String, category: String?, width: Int, height: Int, topLevelBack: Boolean) {
// add params to intent
Intent(context, DemoComposeEmbeddedClipsActivity::class.java).apply {
putExtra("collection", collection)
putExtra("category", intialCategory)
putExtra("width", width)
putExtra("height", height)
putExtra("topLevelBack", topLevelBack)
context.startActivity(this)
}
}
}
}
### StorytellerEmbeddedClipsState
`rememberStorytellerEmbeddedClipsState` is a composable function that creates a `StorytellerEmbeddedClipsState` object that holds the state of the `StorytellerEmbeddedClips` composable.
val state = rememberStorytellerEmbeddedClipsState(collectionId = "collection", topLevelBack = topLevelBack)
StorytellerEmbeddedClips(
modifier = Modifier,
state = state
)
`StorytellerEmbeddedClipsState` contains `canGoBack` property that can be used to check if the fragment can go back from the current Category or is it at the top level.
val state = rememberStorytellerEmbeddedClipsState(collectionId = "collection", topLevelBack = topLevelBack)
val canGoBack = state.canGoBack // true if user can navigate back
`StorytellerEmbeddedClipsState` contains `goBack()` which will move the content to previous Category if the user is not at the top level.
val state = rememberStorytellerEmbeddedClipsState(collectionId = collection, topLevelBack = topLevelBack)
state.goBack() // navigate to previous category programmatically
# Custom Themes Overview
The Storyteller SDK's appearance can be customized by setting the `Storyteller.theme` global property or the `StorytellerListView.theme` view-specific property. This is done using a `UiTheme` configuration object, preferably created with the `buildTheme` DSL.
## Defining a Theme with `buildTheme`
The `buildTheme` method provides a DSL to define theme properties. It supports `light` and `dark` variants. You can define properties for the `light` theme and then easily apply them to the `dark` theme using `dark from light`.
import com.storyteller.domain.theme.builders.buildTheme
import com.storyteller.domain.theme.builders.ofHexCode
import com.storyteller.domain.theme.builders.fontRes // For font resources
import com.storyteller.domain.theme.builders.drawableRes // For drawable resources
Storyteller.theme = buildTheme {
light { // Configure the light theme
colors.primary = ofHexCode("#1A2B3C") // Example: Set primary color
font = fontRes(R.font.your_custom_font_family) // Example: Set a custom font
// ... other light theme customizations
}
dark from light // Apply light theme settings to dark, then override if needed
// dark.colors.primary = ofHexCode("#D3E4F5") // Example: Override primary for dark
}
**Key Points for `buildTheme`:**
- Set the global theme (`Storyteller.theme`) before any Storyteller list views are inflated.
- Theme properties are optional; the SDK provides default values.
- Use `light` and `dark` blocks to specify theme variants.
- The active theme variant is chosen based on the current system UI mode and the `StorytellerListView.uiStyle` property.
## What Can Be Themed?
The `UiTheme` object allows customization of various aspects, broadly categorized into:
- **Colors**: Base colors for UI elements (primary, success, alert, white/black shades for text).
- Set with `ofHexCode("#RRGGBB")` or Android `@ColorInt`.
- **Font**: A global custom font for the UI (requires a font family XML resource).
- Set with `fontRes(R.font.your_font_family)`.
- **Primitives**: Base values like `cornerRadius`.
- **Lists**: Appearance of Story/Clip rows and grids (spacing, insets, background).
- **Story and Clip Tiles**: Appearance of individual tiles (chips, titles, indicators, borders for both circular and rectangular tiles).
- **Player**: Properties of the Story/Clip player (icons, buttons, timestamps, live indicators).
- **Buttons**: General button appearance (background, text color, corner radius).
- **Instructions Screen**: Appearance of the initial instructions screen.
- **Engagement Units**: Styling for Polls and Quizzes (answer colors, result bars).
- **Search**: Customizations for the Search component.
- **Home**: Styling for elements within the Storyteller Home feature.
Many specific properties exist within these categories to allow fine-grained control. Refer to the detailed `UiTheme` structure or the full `Themes.md` documentation for an exhaustive list if deeper customization is needed.
### Setting Specific Property Types
- **Colors**: Use `ofHexCode("#RRGGBB")` or any Android `@ColorInt`.
light.colors.primary = ofHexCode("#FF00FF")
- **Drawables**: For properties expecting a `StorytellerDrawable`, use `drawableRes(R.drawable.your_drawable)`.
light.player.icons.close = drawableRes(R.drawable.custom_close_icon)
- **Fonts**: Define a font family XML (e.g., `res/font/custom_font.xml`) and set it using `fontRes(R.font.custom_font)`.
<!-- res/font/custom_font.xml -->
<?xml version="1.0" encoding="utf-8"?>
<font-family xmlns:android="http://schemas.android.com/apk/res/android">
<font android:fontWeight="400" android:font="@font/my_font_regular" />
<font android:fontWeight="700" android:font="@font/my_font_bold" />
</font-family>
light.font = fontRes(R.font.custom_font)
This summary provides the core information needed to start theming the Storyteller SDK. For detailed property names and advanced configurations, consult the full `Themes.md` documentation.
# Analytics Overview
The Storyteller SDK provides a robust analytics system, triggering various events based on user interactions. These events can be captured by implementing the `StorytellerDelegate`.
## Listening to Analytics Events
To receive analytics events, you need to set a `StorytellerDelegate` on the `Storyteller` object. The primary method for receiving these events is `onUserActivityOccurred`.
// Example of setting up the delegate (typically in your Application class or where Storyteller is initialized)
Storyteller.storytellerDelegate = object : StorytellerDelegate {
// ... other delegate methods
override fun onUserActivityOccurred(type: StorytellerUserActivity.EventType, data: UserActivityData) {
// Handle the analytics event here
// 'type' is an enum member of StorytellerUserActivity.EventType
// 'data' is a UserActivityData object containing details about the event
when (type) {
StorytellerUserActivity.EventType.OPENED_STORY -> {
val storyId = data.storyId
// Process Opened Story event
}
StorytellerUserActivity.EventType.OPENED_CLIP -> {
val clipId = data.clipId
// Process Opened Clip event
}
// ... handle other event types as needed
else -> {
// Optional: handle other events or log them
}
}
}
// ... other delegate methods
}
## Event Categories
The SDK triggers a wide range of events, which can be broadly categorized as:
- **Story Events**: Interactions with Stories, such as `OPENED_STORY`, `OPENED_PAGE`, `DISMISSED_STORY`, `SKIPPED_STORY`, `COMPLETED_PAGE`, `VOTED_POLL`, `TRIVIA_QUIZ_COMPLETED`, etc.
- **Clip Events**: Interactions with Clips, including `OPENED_CLIP`, `DISMISSED_CLIP`, `NEXT_CLIP`, `LIKED_CLIP`, `FOLLOW_CATEGORY`, `ENABLE_CAPTIONS`, etc.
- **Sheet Events**: Events related to opening and dismissing Sheets, like `OPENED_SHEET`, `DISMISSED_SHEET`.
- **Ad Events**: Events specifically for ad playback within Stories or Clips, such as `OPENED_AD`, `DISMISSED_AD`, `SKIPPED_AD`, `AD_ACTION_BUTTON_TAPPED`, and quartile view events (`VIEWED_AD_PAGE_FIRST_QUARTILE`, etc.).
- **Playback Events**: Events related to media playback status, primarily for video content, like `READY_TO_PLAY`, `MEDIA_STARTED`, `BUFFERING_STARTED`, `BUFFERING_ENDED`.
- **Search Events**: User interactions within the Storyteller search feature, including `OPENED_SEARCH`, `PERFORMED_SEARCH`, `USED_SUGGESTION`.
## Event Data (`UserActivityData`)
Each event callback provides a `type` (`StorytellerUserActivity.EventType`) and a `data` object (`UserActivityData`). The `UserActivityData` class contains numerous optional properties that provide context about the event. Common properties include:
- `storyId`, `storyTitle`, `storyIndex`, `storyPageCount`
- `pageId`, `pageIndex`, `pageType`
- `clipId`, `clipTitle`, `clipIndex`
- `collection`, `categories`, `currentCategory`
- `openedReason`, `dismissedReason`
- `durationViewed`, `pagesViewedCount`, `loopsViewed`
- Ad-specific data like `adId`, `advertiserName`, `adType`, `adPlacement`
- And many more specific to certain events (e.g., `pollAnswerId`, `triviaQuizScore`, `searchTerm`).
Refer to the full `Analytics.md` documentation for a complete list of all event types and the specific data properties available for each event if detailed tracking is required.
Here are some effective prompts to use with AI assistants when implementing Storyteller SDK: