In a sealed state type, when should a case be a `data object` versus a `data class` with no meaningful fields, and how does that choice affect equality and StateFlow emissions?
answer
- No payload -> data object (singleton)
- Payload -> data class (structural equals)
- data class with zero params is a compile error
- StateFlow conflates: emits only when != current
- Plain class = identity equals -> spurious emissions
basics
~20 sIf a state carries no data (like Loading), make it a single shared object. If it carries data, make it a data class. A shared object is always equal to itself, which keeps state comparisons and StateFlow updates predictable.
solid answer
~40 sUse a `data object` for stateless cases (Loading, Idle) and a `data class` for cases with a payload (Success(data), Error(message)). A `data object` is a **singleton**: there's exactly one instance, so reference and structural equality coincide and `equals` is trivially true for the same case. A `data class` gets generated structural `equals`/`hashCode` over its properties, so two `Success(sameList)` values are equal. This matters for `StateFlow`, which is **conflated** and only emits when the new value is `!=` the current one (`distinctUntilChanged` semantics): emitting `Loading` twice as a singleton produces no spurious re-emission, and a `data class` payload that didn't change won't re-emit either. A plain `class` (or `object` without `data`) would give identity-only `equals`, causing either missed or duplicate emissions and unreadable `toString()`.
code
kotlin · 10 linessealed interface UiState {
data object Loading : UiState
data class Success(val data: List<Row>) : UiState
data class Error(val message: String) : UiState
}
val flow = MutableStateFlow<UiState>(UiState.Loading)
flow.value = UiState.Loading // no re-emit: equal singleton
flow.value = UiState.Success(rows) // emits
flow.value = UiState.Success(rows) // no re-emit if rows equalgo deeper
Knows Loading should be an object and Success a data class but may not connect it to equality.
Explains singleton vs structural equals and that data class needs at least one param; links to StateFlow conflation.
Reasons about emission correctness, in-place mutation pitfalls, and Compose recomposition driven by equals.
Sets immutability/equality conventions for state holders and audits for plain-class state that breaks diffing.
## The choice ```kotlin sealed interface UiState { data object Loading : UiState // no payload -> singleton data class Success(val data: List<Row>) : UiState data class Error(val message: String) : UiState } ``` - **`data object Loading`**: a singleton — only one instance exists for the whole program. `data` on an object adds a readable `toString()` (`"Loading"`) and a consistent `equals`/`hashCode` (always equal to itself). Introduced as `data object` so you don't hand-write those. - **`data class Success`**: carries data; `data` generates `equals`, `hashCode`, `copy`, `componentN` over the declared properties. Two `Success` values are equal iff their `data` are equal. ## Why not a payload-less data class? A `data class` with no primary-constructor properties is actually a compile **error** (a data class must have at least one parameter). Even if it had a dummy field, every `Loading()` call would allocate a new instance and they'd only be equal via the generated `equals`. A singleton `data object` avoids the allocation and gives identity equality for free — strictly better for stateless cases. ## Equality and `StateFlow` `StateFlow<T>` is **conflated** and compares with `equals`: setting `value` to something `==` the current value emits **nothing** (built-in `distinctUntilChanged`). Two consequences: ```kotlin val flow = MutableStateFlow<UiState>(UiState.Loading) flow.value = UiState.Loading // no emission: same singleton, equal flow.value = UiState.Success(rows) flow.value = UiState.Success(rows) // no emission if rows == rows (data class) ``` - If `Loading` were a plain `object` it would still be a singleton, but a **plain `class`** instance per emission would have identity `equals`, so two 'equal' states would look different and re-emit, causing needless recomposition. - Conversely, if you *override* `equals` incorrectly (e.g. ignore a field), a real change might be swallowed and the UI won't update. ## Rule - Stateless case -> `data object` (singleton, free correct equality, no allocation). - Case with data -> `data class` (structural equality over the payload). - Never a plain `class` for state you compare or push through `StateFlow`/Compose.
- What happens if Success holds a List that you mutate in place instead of replacing?data class equals compares by the list's current contents, but if you mutate the same instance the old and new state may be the same object and StateFlow won't emit. Always create a new immutable list / new Success value so the change is detectable.
- Can you write `data class Loading()` with no properties?No. A data class requires at least one primary-constructor parameter, so the compiler rejects it. Use data object for the stateless case.
Loading is like the single word 'PAUSED' on a screen — one shared label; Success is like a printed receipt whose contents you compare line by line.
saying these in an interview costs you the question
- Using a plain class for state pushed through StateFlow
- Trying to write a data class with no constructor params
- Allocating a new Loading instance each time instead of a singleton
- Not knowing StateFlow conflates by equals
- Mutating a payload list in place and expecting an emission