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