Skip to content
All posts
March 30, 20265 min read

Kotlin Sealed Classes and Interfaces: Beyond the Basics

Sealed classes are more than a fancy enum. Here are the patterns that make them indispensable for Android development: representing results, modeling state machines, handling events, and using sealed interfaces for better composition.

KotlinAndroidArchitecturePatterns
Share:

code
sealed class
is one of Kotlin's most useful features and one of the most underused. Most Android devs know it for representing UI states. Here are the patterns that go further.


The Foundation: Why Sealed Over Enum

Enums are fixed values. Sealed classes are fixed hierarchies where each subtype can carry different data:

kotlin
// Enum — can't carry different data per case
enum class NetworkResult {
    SUCCESS, // Where's the data?
    FAILURE  // Where's the error?
}

// Sealed class — each case carries what it needs
sealed class NetworkResult<out T> {
    data class Success<T>(val data: T) : NetworkResult<T>()
    data class Failure(val code: Int, val message: String) : NetworkResult<Nothing>()
    object Loading : NetworkResult<Nothing>()
}

When you

code
when
over a sealed class, the compiler ensures you've handled every case.


Pattern 1: Result Type

Replace

code
try/catch
scattered through your code with an explicit Result type:

kotlin
sealed class Result<out T> {
    data class Success<T>(val value: T) : Result<T>()
    data class Error(val exception: Exception, val message: String = exception.message ?: "") : Result<Nothing>()
    
    val isSuccess: Boolean get() = this is Success
    val isError: Boolean get() = this is Error
    
    fun getOrNull(): T? = if (this is Success) value else null
    fun getOrThrow(): T = when (this) {
        is Success -> value
        is Error -> throw exception
    }
    
    inline fun onSuccess(action: (T) -> Unit): Result<T> {
        if (this is Success) action(value)
        return this
    }
    
    inline fun onError(action: (Exception) -> Unit): Result<T> {
        if (this is Error) action(exception)
        return this
    }
}

// Usage
suspend fun fetchTask(id: String): Result<Task> {
    return try {
        Result.Success(apiService.getTask(id))
    } catch (e: Exception) {
        Result.Error(e)
    }
}

// In ViewModel
viewModelScope.launch {
    fetchTask(taskId)
        .onSuccess { task -> _uiState.value = UiState.Success(task) }
        .onError { e -> _uiState.value = UiState.Error(e.message ?: "Unknown error") }
}

Pattern 2: State Machines

Sealed classes model state machines explicitly. Every valid state is a subtype. Invalid transitions are compile-time errors:

kotlin
sealed class OrderState {
    object Pending : OrderState()
    data class Processing(val processorId: String) : OrderState()
    data class Shipped(val trackingNumber: String) : OrderState()
    data class Delivered(val deliveredAt: Instant) : OrderState()
    data class Cancelled(val reason: String) : OrderState()
    
    fun canCancel(): Boolean = this is Pending || this is Processing
    fun canShip(): Boolean = this is Processing
}

class OrderStateMachine(initialState: OrderState = OrderState.Pending) {
    var state: OrderState = initialState
        private set
    
    fun cancel(reason: String): Result<OrderState> {
        return if (state.canCancel()) {
            state = OrderState.Cancelled(reason)
            Result.Success(state)
        } else {
            Result.Error(IllegalStateException("Cannot cancel order in state $state"))
        }
    }
    
    fun ship(trackingNumber: String): Result<OrderState> {
        return if (state.canShip()) {
            state = OrderState.Shipped(trackingNumber)
            Result.Success(state)
        } else {
            Result.Error(IllegalStateException("Cannot ship order in state $state"))
        }
    }
}

Pattern 3: Sealed Interfaces for Composition

code
sealed interface
allows a class to implement multiple sealed hierarchies:

kotlin
sealed interface UiEvent
sealed interface NavigationEvent : UiEvent
sealed interface AnalyticsEvent : UiEvent

data class ShowSnackbar(val message: String) : UiEvent
data class NavigateToDetail(val taskId: String) : NavigationEvent, UiEvent
data class TaskCreated(val taskId: String) : AnalyticsEvent, UiEvent

// Handler that processes only navigation events
fun handleNavigation(event: NavigationEvent) {
    when (event) {
        is NavigateToDetail -> navController.navigate("tasks/${event.taskId}")
    }
}

// Handler that processes all events
fun handleEvent(event: UiEvent) {
    when (event) {
        is ShowSnackbar -> showSnackbar(event.message)
        is NavigateToDetail -> navController.navigate("tasks/${event.taskId}")
        is TaskCreated -> analytics.track("task_created", mapOf("id" to event.taskId))
    }
}

A class implementing a

code
sealed interface
can be part of multiple type hierarchies — impossible with
code
sealed class
.


Pattern 4: Exhaustive When as Expression

The most powerful property of sealed classes:

code
when
as an expression must be exhaustive. The compiler tells you when you add a new subtype and forget to handle it:

kotlin
fun describeState(state: OrderState): String = when (state) {
    OrderState.Pending -> "Waiting for payment"
    is OrderState.Processing -> "Being processed by ${state.processorId}"
    is OrderState.Shipped -> "In transit: ${state.trackingNumber}"
    is OrderState.Delivered -> "Delivered at ${state.deliveredAt}"
    is OrderState.Cancelled -> "Cancelled: ${state.reason}"
}
// If you add a new OrderState subtype, this won't compile until you handle it

This is the safety property that makes sealed classes valuable for evolving codebases.


Pattern 5: Hierarchical Sealed Classes

Group related states under a parent:

kotlin
sealed class TaskFilter {
    object All : TaskFilter()
    
    sealed class ByStatus : TaskFilter() {
        object Active : ByStatus()
        object Completed : ByStatus()
    }
    
    sealed class ByPriority : TaskFilter() {
        object High : ByPriority()
        object Medium : ByPriority()
        object Low : ByPriority()
    }
    
    data class ByTag(val tag: String) : TaskFilter()
}

// Handle at any level
fun applyFilter(tasks: List<Task>, filter: TaskFilter): List<Task> = when (filter) {
    TaskFilter.All -> tasks
    is TaskFilter.ByStatus -> when (filter) {
        TaskFilter.ByStatus.Active -> tasks.filter { !it.completed }
        TaskFilter.ByStatus.Completed -> tasks.filter { it.completed }
    }
    is TaskFilter.ByPriority -> tasks.filter { it.priority == filter.toPriority() }
    is TaskFilter.ByTag -> tasks.filter { filter.tag in it.tags }
}

When NOT to Use Sealed Classes

  • Simple flags →
    code
    Boolean
  • Fixed set of named constants with no data →
    code
    enum class
  • Three or fewer simple states →
    code
    enum class
    with data class for state
  • When you need to add subtypes outside the module → use
    code
    abstract class
    or
    code
    interface

Takeaways

  • Sealed classes guarantee exhaustive
    code
    when
    handling — new subtypes cause compile errors, not runtime surprises
  • Result type (sealed class) replaces scattered try/catch with explicit success/failure handling
  • State machines modeled with sealed classes make invalid transitions visible at compile time
  • Sealed interfaces enable multiple type hierarchies — a class can belong to several sealed families
  • Hierarchical sealed classes group related states while preserving exhaustive checking at each level
Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this