Modern examples of 3 practical examples of Kotlin sealed classes in real apps
Real-world examples of 3 practical examples of Kotlin sealed classes
Instead of starting with theory, let’s jump straight into real examples. When people ask for examples of 3 practical examples of Kotlin sealed classes, they usually mean: “Show me the patterns I’ll actually use in production.”
So we’ll center the discussion around three big buckets you hit constantly in Kotlin projects:
- Representing network or data loading results
- Modeling UI state and navigation
- Structuring domain events and errors
Inside each bucket, we’ll walk through multiple real examples, so you get more than just three toy snippets.
Example of sealed classes for network and data results
One of the best examples of 3 practical examples of Kotlin sealed classes is the classic Result pattern. You’ve probably seen a version of this in Android apps using Retrofit, Ktor, or GraphQL.
1. API result wrapper for Retrofit / Ktor
You want every network call to return either a success with data or some flavor of failure, without sprinkling nullable types and unchecked exceptions everywhere.
sealed class ApiResult<out T> {
data class Success<out T>(val data: T) : ApiResult<T>()
data class HttpError(val code: Int, val message: String?) : ApiResult<Nothing>()
data class NetworkError(val throwable: Throwable) : ApiResult<Nothing>()
data class SerializationError(val throwable: Throwable) : ApiResult<Nothing>()
}
suspend fun fetchUser(id: String): ApiResult<User> = try {
val response = api.getUser(id)
if (response.isSuccessful && response.body() != null) {
ApiResult.Success(response.body()!!)
} else {
ApiResult.HttpError(response.code(), response.message())
}
} catch (e: IOException) {
ApiResult.NetworkError(e)
} catch (e: SerializationException) {
ApiResult.SerializationError(e)
}
Because ApiResult is sealed, a when on it must be exhaustive:
fun render(result: ApiResult<User>) = when (result) {
is ApiResult.Success -> showUser(result.data)
is ApiResult.HttpError -> showError("HTTP ${result.code}")
is ApiResult.NetworkError -> showError("Check your connection")
is ApiResult.SerializationError -> showError("Bad response format")
}
This is one of the best examples of sealed classes preventing “forgotten branch” bugs. The compiler forces you to handle every case.
2. Paging and streaming data states
Modern apps rely heavily on infinite scroll and streaming data. Libraries like Android’s Paging 3 already use sealed types internally, and you can mirror that pattern in your domain layer.
sealed class PageResult<out T> {
data class Page<out T>(val items: List<T>, val nextKey: String?) : PageResult<T>()
object EndOfList : PageResult<Nothing>()
data class Error(val throwable: Throwable) : PageResult<Nothing>()
}
suspend fun loadNextPage(key: String?): PageResult<Article> { /* ... */ }
The advantage over a simple nullable approach is that EndOfList is explicit. There’s no guessing whether null means “no more data” or “we messed up the API call.” This is a clean example of how sealed classes encode meaning directly in the type system.
UI state: examples include screens, dialogs, and loading flows
The second cluster of examples of 3 practical examples of Kotlin sealed classes lives in UI state management. With Jetpack Compose and unidirectional data flow, sealed classes shine.
3. Screen state in Jetpack Compose or XML-based UIs
A common pattern in 2024–2025 Android apps is a single UiState sealed class per screen.
sealed class UserProfileUiState {
object Loading : UserProfileUiState()
data class Content(val user: User) : UserProfileUiState()
data class Error(val message: String, val canRetry: Boolean) : UserProfileUiState()
}
@Composable
fun UserProfileScreen(state: UserProfileUiState) {
when (state) {
UserProfileUiState.Loading -> LoadingView()
is UserProfileUiState.Content -> ProfileContent(state.user)
is UserProfileUiState.Error -> ErrorView(state.message, state.canRetry)
}
}
This is a textbook example of a sealed class replacing a messy combination of booleans like isLoading, errorMessage, and user. You can’t accidentally show a loading spinner and content at the same time, because that state simply doesn’t exist in the type.
4. Dialogs and bottom sheets as sealed UI events
Another realistic example of sealed classes in UI is controlling dialogs and bottom sheets.
sealed class DialogState {
object None : DialogState()
object ConfirmLogout : DialogState()
data class DeleteItem(val itemName: String) : DialogState()
}
@Composable
fun AppDialogs(state: DialogState, onDismiss: () -> Unit) {
when (state) {
DialogState.None -> Unit
DialogState.ConfirmLogout -> ConfirmLogoutDialog(onDismiss)
is DialogState.DeleteItem -> DeleteItemDialog(state.itemName, onDismiss)
}
}
Here, the sealed class becomes a single source of truth for all transient dialog states. This pattern scales nicely as your app adds more dialogs over time.
5. Navigation destinations in a single-activity app
Navigation is another area where examples of 3 practical examples of Kotlin sealed classes appear all the time.
sealed class Destination(val route: String) {
object Home : Destination("home")
data class Details(val id: String) : Destination("details/{id}")
object Settings : Destination("settings")
}
fun NavController.navigateTo(destination: Destination) {
when (destination) {
Destination.Home -> navigate("home")
is Destination.Details -> navigate("details/${destination.id}")
Destination.Settings -> navigate("settings")
}
}
Compared to stringly-typed routes, this pattern is far safer. You can’t accidentally navigate to a route that doesn’t exist, and you can’t forget the required arguments.
Domain modeling: errors, payments, and analytics
The third group of examples of 3 practical examples of Kotlin sealed classes lives in the domain layer—places where you care about business meaning, not just UI or transport.
6. Domain-specific error handling
Instead of throwing generic exceptions, you can represent domain errors as sealed classes that calling code must handle.
sealed class PaymentError : Throwable() {
object InsufficientFunds : PaymentError()
object CardExpired : PaymentError()
object FraudSuspected : PaymentError()
data class Unknown(val cause: Throwable) : PaymentError()
}
sealed class PaymentResult {
object Success : PaymentResult()
data class Failure(val error: PaymentError) : PaymentResult()
}
suspend fun chargeCard(request: PaymentRequest): PaymentResult { /* ... */ }
Now your checkout flow can respond precisely:
when (val result = chargeCard(request)) {
PaymentResult.Success -> showReceipt()
is PaymentResult.Failure -> when (result.error) {
PaymentError.InsufficientFunds -> showTopUpPrompt()
PaymentError.CardExpired -> showUpdateCardScreen()
PaymentError.FraudSuspected -> showSupportContact()
is PaymentError.Unknown -> showGenericError()
}
}
This is one of the best examples of Kotlin sealed classes improving real business logic, not just making syntax prettier.
7. Analytics and logging events
Modern apps log a lot of events—to internal dashboards, to tools like Firebase Analytics, or to custom data platforms. Using sealed classes for events gives you type-safe tracking.
sealed class AnalyticsEvent {
data class ScreenView(val name: String) : AnalyticsEvent()
data class ButtonClick(val id: String) : AnalyticsEvent()
data class Purchase(val productId: String, val amountCents: Long) : AnalyticsEvent()
}
interface AnalyticsTracker {
fun track(event: AnalyticsEvent)
}
class FirebaseAnalyticsTracker(/* ... */) : AnalyticsTracker {
override fun track(event: AnalyticsEvent) {
when (event) {
is AnalyticsEvent.ScreenView -> logScreen(event.name)
is AnalyticsEvent.ButtonClick -> logClick(event.id)
is AnalyticsEvent.Purchase -> logPurchase(event.productId, event.amountCents)
}
}
}
Later, if your data team adds a new required field to Purchase, the compiler will guide you to update all the tracking code.
8. Authentication flows and user sessions
Authentication flows are another area where examples of 3 practical examples of Kotlin sealed classes fit perfectly.
sealed class AuthState {
object LoggedOut : AuthState()
object Loading : AuthState()
data class LoggedIn(val userId: String, val token: String) : AuthState()
data class Error(val message: String) : AuthState()
}
class AuthViewModel(/* ... */) {
private val _state = MutableStateFlow<AuthState>(AuthState.LoggedOut)
val state: StateFlow<AuthState> = _state
fun login(username: String, password: String) {
_state.value = AuthState.Loading
// ... perform login, then update state to LoggedIn or Error
}
}
This pattern is now standard in many Android samples and production apps, especially those using Kotlin coroutines and StateFlow.
Why sealed classes instead of enums or interfaces?
Looking at these real examples of 3 practical examples of Kotlin sealed classes, a natural question is: why not enums or plain interfaces?
Enums work well when:
- You only need constant values with minimal data
- The set of options is small and not expected to grow complex
But enums fall short when each case needs different data. For example, PaymentError.CardExpired and PaymentError.Unknown(cause) have very different payloads. With enums, you’d end up with nullable fields or parallel maps.
Interfaces are flexible but too open. Any file can implement an interface, so the compiler can’t guarantee you’ve handled all implementations in a when. With sealed classes, all subclasses must be in the same file (or module, with sealed interface + permits in newer Kotlin), so the compiler can enforce exhaustiveness.
Kotlin’s official documentation on sealed classes explains this closed-hierarchy behavior in more detail: https://kotlinlang.org/docs/sealed-classes.html
2024–2025 trends: where sealed classes are heading
A few trends make these examples of 3 practical examples of Kotlin sealed classes even more relevant in 2024–2025:
- Kotlin Multiplatform: Shared models for UI state and domain errors benefit heavily from sealed classes, because they compile to multiple targets (Android, iOS, desktop) with the same type safety.
- Jetpack Compose & Compose Multiplatform: Declarative UIs love explicit state. Sealed classes fit right into unidirectional data flow.
- Structured concurrency with coroutines: When you combine flows, channels, and sealed event types, your async code becomes a lot more predictable.
If you care about correctness and maintainability—especially in regulated domains like finance or health—modeling state explicitly is not just a nicety. For example, when dealing with health-related apps that must respect privacy and safety guidelines, clear state machines and explicit error types make audits and testing far easier. While not Kotlin-specific, organizations like the U.S. National Institute of Standards and Technology (NIST) publish guidance on secure software engineering that aligns well with these ideas: https://www.nist.gov/itl
FAQ about Kotlin sealed classes with real examples
What are some real examples of Kotlin sealed classes in Android apps?
Real examples include:
UiStatesealed classes for screens (Loading, Content, Error)- Navigation destinations as sealed types with routes and arguments
- Network result wrappers like
ApiResult.SuccessandApiResult.HttpError - Domain-specific errors such as
PaymentError.InsufficientFunds - Analytics events like
AnalyticsEvent.ScreenViewandAnalyticsEvent.Purchase
These map directly to the examples of 3 practical examples of Kotlin sealed classes shown earlier.
Can sealed classes replace the Result type from the Kotlin standard library?
Sometimes. The standard Result<T> is great for simple success/failure flows. But when you need richer error detail or multiple failure modes, defining your own sealed class (like ApiResult<T> or PaymentResult) gives you clearer, self-documenting code.
Are sealed classes good for modeling UI navigation?
Yes. A sealed Destination hierarchy is a clean example of how to avoid stringly-typed navigation. Each destination can carry typed arguments, and your when over Destination is checked at compile time.
Is there a downside to using sealed classes everywhere?
Overusing them can make your model layer noisy. If a state only has two simple options and no extra data, a Boolean or enum might be enough. Reach for sealed classes when:
- Each case carries different data, or
- You want the compiler to enforce exhaustive handling.
Where can I learn more beyond these examples?
Alongside the Kotlin docs, many university courses on programming languages and type systems discuss algebraic data types, which sealed classes approximate. For a broader view of type-safe design, you might explore material from institutions like MIT OpenCourseWare: https://ocw.mit.edu
Sealed classes are not magic, but the patterns above show why developers keep asking for examples of 3 practical examples of Kotlin sealed classes. They give you a way to encode real-world constraints directly into your types—so the compiler can help you keep your app’s behavior honest as it grows.
Related Topics
Real-world examples of diverse Kotlin coroutines in 2025
The best examples of Kotlin RecyclerView – 3 practical use cases for modern Android apps
Modern examples of 3 practical examples of Kotlin sealed classes in real apps
Real‑world examples of diverse Kotlin data classes for modern development
Explore More Kotlin Code Snippets
Discover more examples and insights in this category.
View All Kotlin Code Snippets