Lesson 4 of 83 intermediate

Sealed Classes, Enums, Objects & Companion Objects

Modern Kotlin state modeling patterns for clean, type-safe Android architecture

Open interactive version (quiz + challenge)

Real-world analogy

A sealed class is like a locked filing cabinet where you know EXACTLY what folders can exist inside — no surprises. An enum is a fixed set of named constants, like the days of the week. An object is a single post-office — only one can exist in the whole universe of your app.

What is it?

Sealed classes model bounded type hierarchies where every possible state is known at compile time. Enums model fixed constant sets. Objects are thread-safe singletons. Companion objects provide class-level factory methods and constants. Together, these form the building blocks of type-safe state management in MVVM and MVI architectures.

Real-world relevance

In a real-time SaaS collaboration app, UiState sealed class (Loading/Success/Error) drives the UI layer. NetworkResult sealed class wraps API responses. The enum class SyncStatus tracks document sync state. A companion object factory method creates the ViewModel with proper dependencies. The object SocketManager is the app-wide singleton managing the WebSocket connection.

Key points

Code example

// Sealed class for UI state — the standard pattern
sealed class UiState<out T> {
    object Loading : UiState<Nothing>()
    data class Success<T>(val data: T) : UiState<T>()
    data class Error(val message: String, val cause: Throwable? = null) : UiState<Nothing>()
}

// Sealed interface (Kotlin 1.5+) — more flexible
sealed interface DocumentEvent {
    data class Opened(val docId: String, val userId: String) : DocumentEvent
    data class Edited(val docId: String, val delta: Delta) : DocumentEvent
    object Closed : DocumentEvent
}

// Enum with properties — avoid ordinal for persistence
enum class SyncStatus(val code: String, val isTerminal: Boolean) {
    PENDING("pending", false),
    IN_PROGRESS("in_progress", false),
    SYNCED("synced", true),
    FAILED("failed", true);

    companion object {
        fun fromCode(code: String): SyncStatus =
            values().firstOrNull { it.code == code } ?: PENDING
    }
}

// object singleton — WebSocket manager
object SocketManager {
    private var socket: WebSocket? = null

    fun connect(url: String) { /* ... */ }
    fun disconnect() { socket?.close(1000, "User disconnected") }
}

// companion object factory
class DocumentRepository private constructor(
    private val api: DocumentApi,
    private val db: DocumentDao
) {
    companion object {
        fun create(api: DocumentApi, db: DocumentDao) =
            DocumentRepository(api, db)
    }
}

// Exhaustive when — compiler enforces all cases
fun renderUiState(state: UiState<List<Document>>) = when (state) {
    is UiState.Loading  -> showShimmer()
    is UiState.Success  -> showDocuments(state.data)
    is UiState.Error    -> showError(state.message)
}

Line-by-line walkthrough

  1. 1. sealed class UiState — 'out' makes it covariant; UiState can be used as UiState
  2. 2. object Loading : UiState() — singleton object; Nothing is Kotlin's bottom type, compatible with any UiState
  3. 3. data class Success(val data: T) — each Success carries actual data; type-safe, no casting needed
  4. 4. sealed interface DocumentEvent — allows Opened/Edited to be data classes, Closed to be object; more flexible than sealed class
  5. 5. enum class SyncStatus(val code: String, val isTerminal: Boolean) — each entry has typed properties; use code for persistence, not ordinal
  6. 6. companion object { fun fromCode() } inside enum — factory method on the enum itself; idiomatic Kotlin lookup pattern
  7. 7. object SocketManager — thread-safe singleton; initialized once by JVM class loading; accessible everywhere as SocketManager.connect()
  8. 8. class DocumentRepository private constructor() — private constructor forces use of factory method
  9. 9. companion object { fun create() } — factory method; allows validation before construction; testable by swapping implementation
  10. 10. when(state) { is Loading -> ; is Success -> state.data } — no else needed; compiler verifies all sealed subclasses are handled

Spot the bug

sealed class ApiResult {
    data class Success(val data: String) : ApiResult()
    data class Error(val message: String) : ApiResult()
}

fun handleResult(result: ApiResult) {
    when (result) {
        is ApiResult.Success -> println(result.data)
        // Missing Error case
    }
}

enum class Direction { NORTH, SOUTH, EAST, WEST }

fun saveDirection(dir: Direction) {
    database.save("direction", dir.ordinal)  // bug here
}
Need a hint?
One bug is a missing sealed class branch; one is an ordinal persistence anti-pattern
Show answer
Bug 1: when as a statement (not expression) won't fail at compile time for missing branches — but it's a logic bug. If used as an expression (returning a value), it would be a compile error. Best practice: always handle all cases. Add 'is ApiResult.Error -> println(result.message)'. Bug 2: dir.ordinal stores the position (0,1,2,3). If NORTHWEST is added between NORTH and SOUTH, existing saved '1' now maps to NORTHWEST not SOUTH. Fix: database.save('direction', dir.name) stores the stable string 'NORTH', 'SOUTH', etc.

Explain like I'm 5

Sealed class is like a board game with EXACTLY 4 types of cards — you know every possible card type ahead of time, so when you draw one, you always know how to play it. Enum is like a traffic light — exactly 3 states, always the same shape. Object is like the principal's office — there's only ONE, and everyone in school knows where it is.

Fun fact

Google's official Android architecture samples and Compose documentation use the sealed class UiState pattern as the standard way to model screen states. This pattern has become so universal that interviewers often ask candidates to implement it from memory as a live coding exercise.

Hands-on challenge

Model the full state for a document collaboration screen in a SaaS app. Create: 1) A sealed class DocumentUiState with Loading, Success(data, lastUpdated: Long), Error(message, isRetryable: Boolean). 2) A sealed interface UserAction for LoadDocument(id), EditContent(delta), ShareDocument(emails: List), and CloseDocument. 3) An enum class CollaboratorRole(val permissions: Set) with VIEWER, COMMENTER, EDITOR, OWNER. Write a processAction() function with exhaustive when.

More resources

Open interactive version (quiz + challenge) ← Back to course: Android Interview Mastery