Skip to content
AB

Designing State, Intent, and Side-Effect Contracts in Kotlin

Give Compose features an explicit vocabulary for durable UI state, user input, and one-time work.

Ashish BhakhandPublished

A good feature contract makes legal events and states visible before reading its ViewModel.

Code
data class ProfileState(
    val profile: Profile? = null,
    val saving: Boolean = false,
    val fieldErrors: Map<String, String> = emptyMap(),
)

sealed interface ProfileIntent {
    data object Load : ProfileIntent
    data class NameChanged(val value: String) : ProfileIntent
    data object SaveTapped : ProfileIntent
}

sealed interface ProfileEffect {
    data object Saved : ProfileEffect
    data class OpenUrl(val url: String) : ProfileEffect
}

State must survive recomposition and often process recreation. Effects represent consumable instructions. A snackbar stored indefinitely in state can reappear after rotation; a validation message should remain until corrected.

Name intents after events, not implementation setters. SaveTapped gives the ViewModel room to validate, persist, and emit a result. SetLoading exposes internal mechanics to the UI.

Sealed interfaces make exhaustive when expressions possible and keep payloads typed. Avoid a generic OnClick(id: String) that pushes parsing back into the ViewModel.

Contracts are most valuable as boundaries. The composable sends intents and renders state; it never needs to know which repository or dispatcher produces the result.

← Back to all writing