States - Get Started
A State captures additional context that is recorded alongside your session. Unlike a one-time Event, a State's value changes over time — the SDK builds a timeline of those changes and displays it during Session Replay. States can represent anything meaningful: the current UI configuration, feed content, user preferences, network responses, or any other runtime data.
Each custom State is defined by a class that implements the CorynState interface. Custom State identifiers are separate from built-in SDK States.
Defining a State
Your State class must:
- Implement
CorynStateand overridetoJsonString()to serialise itself to a JSON string. - Correctly implement
equals()so the SDK can detect actual changes and avoid redundant serialisation.
A Kotlin data class derives equality from its constructor properties. Include every value that affects the JSON, and keep the returned object graph stable:
import ai.coryn.replay.shared.state.CorynState
import kotlinx.serialization.json.Json
import kotlinx.serialization.Serializable
@Serializable
internal data class SettingsState(
val theme: AppTheme,
val language: String,
val ageSegment: AgeSegment
) : CorynState {
override fun toJsonString(): String {
return Json.encodeToString(serializer(), this)
}
}
equals() and toJsonString()Equality must reflect the JSON value: object key order does not matter, but array order does. Repeatedly returning unequal States that produce the same JSON can cancel recording to prevent unnecessary processing and battery drain.
States and Events share an approximate 200 KB budget per 15-second recording segment, excluding UI captures. Exceeding this budget can omit updates until the next segment. Keep each snapshot small; a single oversized State can still use substantial memory during serialisation.
Use CorynStateMap for small States only — it uses reflection for JSON serialisation. For larger States, prefer a dedicated data class that implements CorynState with Kotlinx Serialization, Moshi code-gen, or a similar approach.
Stable snapshots
Once getState() returns a State, do not mutate that instance or anything reachable from it. The SDK retains it for comparison and serialises it on a worker thread. Changing var properties, mutable collection entries, or nested objects can hide updates, race with serialisation, or repeatedly violate equality/JSON consistency. Defensive error limits can then cancel recording to protect the user's battery.
Return a new snapshot when data changes. val alone is not enough if it refers to a mutable object:
@Serializable
data class SelectionState(val selectedIds: List<String>) : CorynState {
override fun toJsonString(): String = Json.encodeToString(serializer(), this)
}
// Copy the collection so subsequent app changes cannot mutate this snapshot.
override fun getState(): SelectionState = SelectionState(selectedIds.toList())
If collection elements themselves are mutable, copy them into immutable snapshot values too. A shallow toList() or toMap() does not copy nested objects. The same contract applies to States attached to Events.
JSON validation
toJsonString() must return valid JSON, be deterministic and be safe to call on a background thread. Invalid snapshots are skipped and reported as integration errors. Repeated invalid manually supplied States or Event payloads can cancel recording.
If a provider's getState() or serializer throws, its sample is skipped and Coryn retries it later. Other providers continue normally. Use the development Log Viewer to check custom providers before release.
State Providers
A State Provider supplies the current State value to the SDK on demand. getState() runs frequently on the Main Thread while recording; its call frequency is not guaranteed. Keep it non-blocking and return immediately.
- XML Views
- Compose
Implement CorynStateProvider<S> and override getState(). Register it with Coryn.provide():
class UserSettingsActivity : AppCompatActivity(), CorynStateProvider<UserProfileState> {
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
Coryn.provide("UserProfile", this)
}
override fun getState() = UserProfileState(
firstname = binding?.firstname?.text?.toString() ?: "",
surname = binding?.surname?.text?.toString() ?: "",
avatarUrl = binding?.avatar?.url ?: ""
)
...
}
You can also implement the provider in a separate class. onProviderRemove() is optional and is called on the Main Thread when Coryn removes the provider (for example to clean up listeners). It is also called immediately if the recorder dependency is missing and registration cannot complete:
class UserSettingsStateProvider : CorynStateProvider<UserProfileState> {
override fun getState(): UserProfileState {
return UserProfileState(...)
}
override fun onProviderRemove() {
// Clean up resources associated with this provider.
}
}
Coryn.provide(stateId, provider, lifecycle) takes these arguments; Activity and Fragment overloads supply the appropriate lifecycle:
| Argument | Type | Description |
|---|---|---|
stateId | String | Human-readable identifier shown in the replay player, e.g. "Feed", "UserProfile". |
provider | CorynStateProvider | The provider instance. |
lifecycle | Lifecycle | Used to register and unregister the provider automatically. |
When an Activity or Fragment implements CorynStateProvider, use Coryn.provide(stateId, this). The ID is still explicit. Register a Fragment provider in onViewCreated(): it belongs to the view lifecycle, and registration is skipped if no current view exists.
Only one provider can own a stateId at a time. Registering another replaces the previous provider and invokes its removal callback. Keep cleanup scoped to the removed provider's resources.
Manual lifecycle management:
If no Lifecycle is available, use Coryn.provideManualScopeStateProvider(stateId, provider) and call Coryn.removeManualScopeStateProvider(stateId) when the provider is no longer needed.
Use CorynStateProviderRoot to declare a State Provider inline within your Composable tree (requires the Compose dependency from Get Started):
@Composable
private fun Surname() {
var firstname by remember { mutableStateOf("") }
var surname by remember { mutableStateOf("") }
var avatarUrl by remember { mutableStateOf("") }
CorynStateProviderRoot<UserProfileState>("UserProfile") {
provideCorynState { UserProfileState(firstname = firstname, surname = surname, avatarUrl = avatarUrl) }
...
}
}
CorynStateProviderRoot registers the provider automatically when the Composable enters the composition and removes it when it leaves — no manual registration needed.
To also register a Screen without double nesting, use CorynScreenStateProviderRoot:
CorynScreenStateProviderRoot<LinkedOffersState>(
screenName = "OfferDetails",
arguments = bundleOf("title" to "Gothic 2"),
stateId = "LinkedOffers"
) {
...
provideCorynState { LinkedOffersState(...) }
doOnProviderRemove {
// Clean up resources associated with this provider.
}
...
}
doOnProviderRemove(...) is optional. Each call replaces the previously registered cleanup callback. It runs on the Main Thread.
Minimum Compose 1.8.0 (BOM 2025.04.01); recommended 1.9.3 (BOM 2025.10.00) or newer — see Introduction.
Map-based States
For simple or ad-hoc States, CorynStateMap provides a ready-to-use CorynState backed by Map<String, Any?>. No custom class required.
The constructor copies the outer map. Nested values are still shared and must obey the stable snapshot contract. copy() also makes a shallow outer-map copy. Equality compares map entries regardless of insertion order; list/array ordering in the JSON remains meaningful.
Add the corresponding optional dependency first (see Get Started):
- Gson
- Moshi
import ai.coryn.replay.api.stateserialization.gson.model.CorynStateMap
import ai.coryn.replay.api.stateserialization.gson.model.CorynStateMap.Companion.corynStateMapOf
val state = CorynStateMap(
"title" to "Gothic 2: Night of the Raven",
"linkedOffers" to 4,
"isFavourite" to true
)
// or using the helper
val state = corynStateMapOf(
"title" to "Gothic 2: Night of the Raven",
"linkedOffers" to 4,
"isFavourite" to true
)
import ai.coryn.replay.api.stateserialization.moshi.model.CorynStateMap
import ai.coryn.replay.api.stateserialization.moshi.model.CorynStateMap.Companion.corynStateMapOf
val state = CorynStateMap(
"title" to "Gothic 2: Night of the Raven",
"linkedOffers" to 4,
"isFavourite" to true
)
// or using the helper
val state = corynStateMapOf(
"title" to "Gothic 2: Night of the Raven",
"linkedOffers" to 4,
"isFavourite" to true
)
CorynStateMap can be returned directly from a State Provider:
- XML Views
- Compose
import ai.coryn.replay.api.stateserialization.moshi.model.CorynStateMap.Companion.corynStateMapOf
import ai.coryn.replay.api.stateserialization.moshi.provider.CorynMapStateProvider
class OfferDetailsFragment : Fragment(R.layout.offer_details), CorynMapStateProvider {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
Coryn.provide("OfferDetails", this)
}
override fun getState() = corynStateMapOf(
"title" to "Gothic 2: Night of the Raven",
"linkedOffers" to 4,
"isFavourite" to true
)
}
import ai.coryn.replay.api.stateserialization.moshi.model.CorynStateMap
import ai.coryn.replay.api.stateserialization.moshi.model.CorynStateMap.Companion.corynStateMapOf
CorynScreenStateProviderRoot<CorynStateMap>(
screenName = "OfferDetails",
arguments = bundleOf("title" to "Gothic 2"),
stateId = "OfferDetailsState"
) {
var isFavourite by remember { mutableStateOf(false) }
...
provideCorynState {
corynStateMapOf(
"title" to "Gothic 2: Night of the Raven",
"linkedOffers" to 4,
"isFavourite" to isFavourite
)
}
...
}
Related
- Events — one-time captures for user actions or business metrics
- Screens — track screen-level navigation in the session timeline
- Screen Text — automatic text collection and privacy
- Automatic capture — runtime context captured without custom providers