Chapter 12 of 12 — Final! ~14 min read

Sealed Classes & when

The most powerful state modeling pattern in Kotlin — when expressions and sealed classes for exhaustive, compile-safe state.


The when Expression

Kotlin's when replaces Java's switch — but it's far more powerful. It can match types, ranges, conditions, and be used as an expression:

Java switch
String day = "Monday";
String type;
switch (day) {
    case "Monday":
    case "Tuesday":
    case "Wednesday":
    case "Thursday":
    case "Friday":
        type = "Weekday";
        break;
    case "Saturday":
    case "Sunday":
        type = "Weekend";
        break;
    default:
        type = "Unknown";
}

// switch is a statement, not an expression
Kotlin when
val day = "Monday"

// when IS an expression — can assign!
val type = when (day) {
    "Saturday", "Sunday" -> "Weekend"
    "Monday", "Tuesday",
    "Wednesday", "Thursday",
    "Friday" -> "Weekday"
    else -> "Unknown"
}

// when with ranges and conditions:
val score = 85
val grade = when {
    score >= 90 -> "A"
    score >= 80 -> "B"
    score >= 70 -> "C"
    else        -> "F"
}

when with Type Matching

Java
Object obj = getObject();
if (obj instanceof String) {
    System.out.println(((String) obj).length());
} else if (obj instanceof Integer) {
    System.out.println((Integer) obj * 2);
} else {
    System.out.println("Unknown");
}
Kotlin — smart cast inside when
val obj: Any = getObject()
when (obj) {
    is String  -> println(obj.length)  // smart cast!
    is Int     -> println(obj * 2)     // smart cast!
    is Boolean -> println(if(obj) "yes" else "no")
    else       -> println("Unknown")
}

Sealed Classes — Exhaustive State

A sealed class restricts its subclasses to a known set — perfect for modeling UI state or network results. The compiler knows every possible subtype:

Java — error-prone
// Java enum can't carry data
public enum UiState {
    LOADING, SUCCESS, ERROR
}

// OR verbose class hierarchy with instanceof:
if (state instanceof Loading) { ... }
else if (state instanceof Success) {
    Success s = (Success) state;
    showData(s.getData());
} else if (state instanceof Error) { ... }
// Easy to forget a branch — no compile error!
Kotlin — compile-safe
// Sealed class — each subtype can carry data
sealed class UiState {
    object Loading : UiState()
    data class Success(val data: List<User>) : UiState()
    data class Error(val message: String) : UiState()
}

// when is EXHAUSTIVE — compiler checks all branches!
when (val state = uiState.value) {
    is UiState.Loading  -> showLoading()
    is UiState.Success  -> showData(state.data)
    is UiState.Error    -> showError(state.message)
    // No 'else' needed — compiler knows all subtypes
    // Forget a branch? Compile error! ✅
}
Exhaustive when — the killer feature

When you use when as an expression (assigning its result to a variable), the Kotlin compiler enforces exhaustiveness. Add a new subclass to your sealed class and the compiler immediately flags all unhandled when branches. This is impossible in Java.

Sealed Classes in Android — Real-world Pattern

Kotlin — Android ViewModel + sealed class
// Define states
sealed class HomeState {
    object Loading : HomeState()
    data class Content(val users: List<User>) : HomeState()
    data class Error(val msg: String) : HomeState()
    object Empty : HomeState()
}

// ViewModel emits states
class HomeViewModel : ViewModel() {
    private val _state = MutableStateFlow<HomeState>(HomeState.Loading)
    val state = _state.asStateFlow()

    fun loadUsers() {
        viewModelScope.launch {
            _state.value = HomeState.Loading
            try {
                val users = repository.getUsers()
                _state.value = if (users.isEmpty())
                    HomeState.Empty
                else
                    HomeState.Content(users)
            } catch (e: Exception) {
                _state.value = HomeState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

// Composable renders each state
@Composable
fun HomeScreen(state: HomeState) {
    when (state) {
        is HomeState.Loading -> CircularProgressIndicator()
        is HomeState.Content -> UserList(state.users)
        is HomeState.Error   -> ErrorMessage(state.msg)
        is HomeState.Empty   -> EmptyState()
    }
}

Sealed Interface (Kotlin 1.5+)

Kotlin — sealed interface for multi-inheritance
// Sealed interface — subclasses can implement other interfaces
sealed interface Result<out T> {
    data class Success<T>(val data: T) : Result<T>
    data class Failure(val error: Throwable) : Result<Nothing>
    object Loading : Result<Nothing>
}

// Generic result type — very common pattern:
suspend fun fetchUser(): Result<User> = try {
    Result.Success(api.getUser())
} catch (e: Exception) {
    Result.Failure(e)
}
🎉

Course Complete!

You've completed all 12 chapters of Kotlin for Java Developers. You now know the essential patterns to write production-quality Kotlin for Android.