Sealed Classes and Their Magic
Sealed classes in Kotlin provide a way to represent restricted class hierarchies, where all possible subclasses are known at compile time.
Basic Usage
Simple Sealed Class
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val message: String) : Result<Nothing>()
object Loading : Result<Nothing>()
}
// Usage
fun handleResult(result: Result<User>) {
when (result) {
is Result.Success -> println("User: ${result.data}")
is Result.Error -> println("Error: ${result.message}")
Result.Loading -> println("Loading...")
}
}
Sealed Class with Properties
sealed class Shape {
abstract val area: Double
data class Circle(val radius: Double) : Shape() {
override val area: Double
get() = Math.PI * radius * radius
}
data class Rectangle(val width: Double, val height: Double) : Shape() {
override val area: Double
get() = width * height
}
}
// Usage
fun calculateArea(shape: Shape): Double {
return shape.area
}
Advanced Usage
Sealed Class with Functions
sealed class NetworkResponse<out T> {
data class Success<T>(val data: T) : NetworkResponse<T>()
data class Error(val code: Int, val message: String) : NetworkResponse<Nothing>()
fun <R> map(transform: (T) -> R): NetworkResponse<R> {
return when (this) {
is Success -> Success(transform(data))
is Error -> Error(code, message)
}
}
}
// Usage
val response: NetworkResponse<User> = // ...
val mapped = response.map { it.name }
Nested Sealed Classes
sealed class ViewState {
sealed class Data : ViewState() {
data class Success(val items: List<Item>) : Data()
data class Error(val message: String) : Data()
}
object Loading : ViewState()
object Empty : ViewState()
}
// Usage
fun handleViewState(state: ViewState) {
when (state) {
is ViewState.Data.Success -> showItems(state.items)
is ViewState.Data.Error -> showError(state.message)
ViewState.Loading -> showLoading()
ViewState.Empty -> showEmpty()
}
}
Best Practices
- Use sealed classes for restricted hierarchies
- Keep subclasses in the same file
- Use data classes for subclasses when appropriate
- Consider using objects for singleton states
- Document the purpose of each subclass
Common Patterns
State Management
sealed class ScreenState {
data class Content(val data: List<Item>) : ScreenState()
data class Error(val message: String) : ScreenState()
object Loading : ScreenState()
object Empty : ScreenState()
}
class ViewModel {
private val _state = MutableStateFlow<ScreenState>(ScreenState.Loading)
val state: StateFlow<ScreenState> = _state.asStateFlow()
fun loadData() {
_state.value = ScreenState.Loading
// Load data
}
}
API Responses
sealed class ApiResponse<out T> {
data class Success<T>(val data: T) : ApiResponse<T>()
data class Error(val code: Int, val message: String) : ApiResponse<Nothing>()
object Loading : ApiResponse<Nothing>()
fun <R> map(transform: (T) -> R): ApiResponse<R> {
return when (this) {
is Success -> Success(transform(data))
is Error -> Error(code, message)
Loading -> Loading
}
}
}
Performance Considerations
- Sealed classes have minimal runtime overhead
- Compile-time type checking
- No reflection needed
- Efficient when expressions
Common Mistakes
- Not using sealed classes for restricted hierarchies
- Putting subclasses in different files
- Using sealed classes when enums would suffice
- Not handling all cases in when expressions
Conclusion
Sealed classes are a powerful feature in Kotlin that helps create type-safe hierarchies and exhaustive when expressions. Use them to represent restricted class hierarchies and make your code more maintainable.