In one of the Android chats I saw a question: what's better for screen state: sealed or data class? And there were answers like it depends.
Usually I answer all questions of this nature the same way. But here I have a specific answer, so I became interested to generally figure out where this came from and why people continue to use sealed classes for states. Some herd effect. Superficial googling led me to dozens of articles where people talk about Loading-Content-Error with sealed classes. In all examples - LCE. As if there are no other screens at all. I remember five years ago, when I was getting into MVI with what difficulty I tried to realize that you can make state not a sealed class, because all materials showed only this variant.
Who even started this? I remember even before the times of widespread Kotlin in Jake Wharton's epochal talk there was a prototype of this outrage using static factory methods in Java. This, admittedly, is the oldest I remembered. Surely the idea wasn't new even then. And this really at first glance in Kotlin fits best on sealed. With one but, with that approach you didn't need to wrap everything in when to get any field. And that's a wild amount of boilerplate.
Don't get me wrong, I love sealed classes. In my pet projects I literally now make my own internal Result in every use case to beautifully be able (and need) to handle the result or error from outside. Internet and database requests also always sealed wrappers. So it's hard to call me a hater.
But describing screen state with sealed is most often many times more complex than with a data class. Let's see what Google itself writes. nowinandroid:
sealed interface InterestsUiState {
object Loading : InterestsUiState
data class Interests(val topics: List<FollowableTopic>) : InterestsUiState
object Empty : InterestsUiState
}
Everything like in these very textbooks. Let's try to scale. Suppose we need to add to this screen a state in which pull-to-refresh is shown over the data list. What should we do? Add a Boolean field to Interests? Add a new data class InterestsRefreshing? All solutions seem so-so. And this is a screen whose state is described by three fields. Each next field will add so many new variations to the screen that you either won't be able to implement this with sealed descendants, or it will be achieved by duplicating a bunch of code in these descendants. In a degenerate case, you'll get a sealed class with one descendant - a data class that contains all fields. What we fought for, that's what we ran into, as they say.
The same example as a data class is many times more extensible and concise code:
data class InterestsUiState(
val isLoading: Boolean = false,
val topics: List<FollowableTopic>? = null,
)
Copying - great, adding fields - easy, extracting fields - elementary, less code to write - class.
In the pluses of sealed, of course, you can record the impossibility of transitioning to an incorrect state. But this is the only plus in my opinion, transitioning to a correct state is also quite an adventure. Often because of this they say that sealed is better for small screens, with which I can also argue. Adding any field is most often easier to implement by rewriting sealed to data refactoring a bunch of already written code. So what's the point of not writing like that right away?
To summarize, I would just recommend not trying to fit a square peg in a round hole, if you have an obvious subset of descendants that will never be extended - go with sealed. Otherwise always a data class.