Kotlin Sealed Classes

Restricted class hierarchies for type-safe design

🔒 What are Sealed Classes?

Sealed classes in Kotlin represent restricted class hierarchies where all subclasses are known at compile time. They're perfect for representing finite sets of types, enabling exhaustive when expressions and providing type safety for state management and result handling.


// Sealed class example
sealed class Result {
    data class Success(val data: String) : Result()
    data class Error(val message: String) : Result()
}
                                    

Sealed Class Features

🔒

Restricted Hierarchy

Limited set of subclasses

sealed class State {
    object Loading : State()
    data class Success(val data: String) : State()
}
✅

Exhaustive When

Compiler ensures all cases covered

when (state) {
    is State.Loading -> "Loading..."
    is State.Success -> state.data
    // No else needed!
}
🎯

Type Safety

Compile-time type checking

fun handle(result: Result) = when (result) {
    is Result.Success -> result.data
    is Result.Error -> result.message
}
📦

Data & Object Classes

Can contain data classes and objects

sealed class UI {
    object Loading : UI()
    data class Content(val items: List<String>) : UI()
}

🔹 Basic Sealed Class

Here's how to create and use a sealed class for handling different states:

sealed class NetworkResult {
    data class Success(val data: String) : NetworkResult()
    data class Error(val exception: String) : NetworkResult()
    object Loading : NetworkResult()
}

fun handleNetworkResult(result: NetworkResult): String {
    return when (result) {
        is NetworkResult.Success -> "Data received: ${result.data}"
        is NetworkResult.Error -> "Error occurred: ${result.exception}"
        NetworkResult.Loading -> "Loading data..."
    }
}

fun main() {
    val results = listOf(
        NetworkResult.Loading,
        NetworkResult.Success("User data loaded"),
        NetworkResult.Error("Network timeout")
    )
    
    results.forEach { result ->
        println(handleNetworkResult(result))
    }
}

Output:

Loading data...
Data received: User data loaded
Error occurred: Network timeout

🔹 Sealed Classes for UI States

Perfect for managing different UI states in applications:

sealed class UiState {
    object Initial : UiState()
    object Loading : UiState()
    data class Success(val items: List<String>) : UiState()
    data class Error(val message: String) : UiState()
    object Empty : UiState()
}

class ViewModel {
    private var currentState: UiState = UiState.Initial
    
    fun loadData() {
        currentState = UiState.Loading
        displayState()
        
        // Simulate data loading
        val success = (1..10).random() > 3
        
        currentState = if (success) {
            val items = listOf("Item 1", "Item 2", "Item 3")
            if (items.isEmpty()) UiState.Empty else UiState.Success(items)
        } else {
            UiState.Error("Failed to load data")
        }
        
        displayState()
    }
    
    private fun displayState() {
        val message = when (currentState) {
            UiState.Initial -> "Ready to load data"
            UiState.Loading -> "Loading..."
            is UiState.Success -> "Loaded ${currentState.items.size} items: ${currentState.items.joinToString()}"
            is UiState.Error -> "Error: ${currentState.message}"
            UiState.Empty -> "No data available"
        }
        println(message)
    }
}

fun main() {
    val viewModel = ViewModel()
    repeat(3) {
        println("--- Attempt ${it + 1} ---")
        viewModel.loadData()
        println()
    }
}

Output:

--- Attempt 1 ---
Loading...
Loaded 3 items: Item 1, Item 2, Item 3

--- Attempt 2 ---
Loading...
Error: Failed to load data

--- Attempt 3 ---
Loading...
Loaded 3 items: Item 1, Item 2, Item 3

🔹 Sealed Classes with Inheritance

Sealed classes can have abstract methods and properties:

sealed class Shape {
    abstract val area: Double
    abstract fun describe(): String
    
    data class Circle(val radius: Double) : Shape() {
        override val area: Double = Math.PI * radius * radius
        override fun describe() = "Circle with radius $radius"
    }
    
    data class Rectangle(val width: Double, val height: Double) : Shape() {
        override val area: Double = width * height
        override fun describe() = "Rectangle ${width}x${height}"
    }
    
    data class Triangle(val base: Double, val height: Double) : Shape() {
        override val area: Double = 0.5 * base * height
        override fun describe() = "Triangle with base $base and height $height"
    }
}

fun analyzeShape(shape: Shape) {
    println(shape.describe())
    println("Area: ${String.format("%.2f", shape.area)}")
    
    val category = when (shape) {
        is Shape.Circle -> "Curved shape"
        is Shape.Rectangle -> if (shape.width == shape.height) "Square" else "Rectangular shape"
        is Shape.Triangle -> "Angular shape"
    }
    println("Category: $category")
    println()
}

fun main() {
    val shapes = listOf(
        Shape.Circle(5.0),
        Shape.Rectangle(4.0, 6.0),
        Shape.Rectangle(5.0, 5.0),
        Shape.Triangle(3.0, 4.0)
    )
    
    shapes.forEach { analyzeShape(it) }
}

Output:

Circle with radius 5.0
Area: 78.54
Category: Curved shape

Rectangle 4.0x6.0
Area: 24.00
Category: Rectangular shape

Rectangle 5.0x5.0
Area: 25.00
Category: Square

🔹 Sealed Classes vs Enums

Understanding when to use sealed classes instead of enums:

// Enum - for simple constants
enum class Color(val hex: String) {
    RED("#FF0000"),
    GREEN("#00FF00"),
    BLUE("#0000FF")
}

// Sealed class - for complex types with data
sealed class PaymentMethod {
    object Cash : PaymentMethod()
    data class CreditCard(val number: String, val expiryDate: String) : PaymentMethod()
    data class BankTransfer(val accountNumber: String, val bankCode: String) : PaymentMethod()
    data class DigitalWallet(val walletId: String, val provider: String) : PaymentMethod()
}

fun processPayment(method: PaymentMethod, amount: Double): String {
    return when (method) {
        PaymentMethod.Cash -> 
            "Processing cash payment of $${amount}"
        
        is PaymentMethod.CreditCard -> 
            "Charging $${amount} to card ending in ${method.number.takeLast(4)}"
        
        is PaymentMethod.BankTransfer -> 
            "Transferring $${amount} from account ${method.accountNumber}"
        
        is PaymentMethod.DigitalWallet -> 
            "Processing $${amount} payment via ${method.provider} wallet"
    }
}

fun main() {
    val payments = listOf(
        PaymentMethod.Cash,
        PaymentMethod.CreditCard("1234567890123456", "12/25"),
        PaymentMethod.BankTransfer("ACC123456", "BANK001"),
        PaymentMethod.DigitalWallet("wallet123", "PayPal")
    )
    
    payments.forEach { method ->
        println(processPayment(method, 99.99))
    }
    
    // Enum usage
    println("\nAvailable colors:")
    Color.values().forEach { color ->
        println("${color.name}: ${color.hex}")
    }
}

Output:

Processing cash payment of $99.99
Charging $99.99 to card ending in 3456
Transferring $99.99 from account ACC123456
Processing $99.99 payment via PayPal wallet

Available colors:
RED: #FF0000
GREEN: #00FF00
BLUE: #0000FF

🧠 Test Your Knowledge

What's the main advantage of sealed classes over regular inheritance?