Sealed Class trong Kotlin
🎯 Mục tiêu: Hiểu Sealed Class - restricted class hierarchy cho type-safe pattern matching, với các use cases phổ biến như Result và UI State.
💡 Sealed Class là gì?
Sealed class giới hạn hierarchy - compiler biết tất cả subclasses tại compile time. Điều này cho phép exhaustive when (không cần else).
sealed class Result {
data class Success(val data: String) : Result()
data class Error(val message: String) : Result()
data object Loading : Result()
}
fun handleResult(result: Result): String = when (result) {
is Result.Success -> "Data: ${result.data}"
is Result.Error -> "Error: ${result.message}"
Result.Loading -> "Loading..."
// ✅ Không cần else - compiler biết đã cover hết!
}📝 Tại sao cần Sealed Class?
Vấn đề với Open Classes
// ❌ Với open class, bất kỳ ai cũng có thể extend
open class Response
class Success : Response()
class Error : Response()
fun handle(response: Response) = when (response) {
is Success -> "OK"
is Error -> "Failed"
else -> "???" // Phải có else vì có thể có subclass khác!
}
// Ai đó ở module khác có thể:
class HackedResponse : Response() // Compiler không biết!Giải pháp: Sealed Class
// ✅ Sealed class - chỉ subclasses trong cùng file/module
sealed class Response {
data class Success(val data: String) : Response()
data class Error(val code: Int, val message: String) : Response()
data object Loading : Response()
}
fun handle(response: Response) = when (response) {
is Response.Success -> "OK: ${response.data}"
is Response.Error -> "Failed [${response.code}]: ${response.message}"
Response.Loading -> "Loading..."
// ✅ Không cần else - compiler đảm bảo exhaustive!
}💡
Khi add subclass mới: Compiler sẽ báo lỗi ở mọi when chưa handle case mới → không bao giờ miss!
🔧 Cú pháp và Variants
Subclasses có thể là:
sealed class Event {
// data class - có data
data class Click(val x: Int, val y: Int) : Event()
// data object - singleton, không có data (Kotlin 1.9+)
data object Cancel : Event()
// object - singleton (trước Kotlin 1.9)
object Timeout : Event()
// class - có thể mutable
class LongPress(var duration: Long) : Event()
}Nested vs Separate Classes
// Option 1: Nested (recommended for related types)
sealed class Result {
data class Success(val value: Int) : Result()
data class Failure(val error: Throwable) : Result()
}
// Option 2: Separate (same file or module in Kotlin 1.5+)
sealed class NetworkResult
data class NetworkSuccess(val data: String) : NetworkResult()
data class NetworkError(val code: Int) : NetworkResult()
object NetworkLoading : NetworkResult()🎯 Sealed Class vs Enum
| Feature | Sealed Class | Enum |
|---|---|---|
| State riêng | ✅ Mỗi subclass có data riêng | ❌ Cùng structure |
| Multiple instances | ✅ Có thể tạo nhiều instances | ❌ Singleton per value |
| Subclass types | class, data class, object | Chỉ constants |
| Exhaustive when | ✅ Có | ✅ Có |
| Use case | Complex states với data | Simple finite set |
// ❌ Enum không phù hợp - mỗi case cần data khác nhau
enum class BadResult {
SUCCESS, // Cần data?
ERROR, // Cần message và code?
LOADING // Không cần gì
}
// ✅ Sealed class - mỗi case có structure riêng
sealed class GoodResult<out T> {
data class Success<T>(val data: T) : GoodResult<T>()
data class Error(val code: Int, val message: String) : GoodResult<Nothing>()
data object Loading : GoodResult<Nothing>()
}📱 Use Case: UI State
Pattern phổ biến trong Android/Compose:
sealed class UiState<out T> {
data object Loading : UiState<Nothing>()
data class Success<T>(val data: T) : UiState<T>()
data class Error(val message: String) : UiState<Nothing>()
data object Empty : UiState<Nothing>()
}
// ViewModel
class UserViewModel : ViewModel() {
private val _state = MutableStateFlow<UiState<User>>(UiState.Loading)
val state: StateFlow<UiState<User>> = _state.asStateFlow()
fun loadUser(id: Long) {
viewModelScope.launch {
_state.value = UiState.Loading
try {
val user = repository.getUser(id)
if (user != null) {
_state.value = UiState.Success(user)
} else {
_state.value = UiState.Empty
}
} catch (e: Exception) {
_state.value = UiState.Error(e.message ?: "Unknown error")
}
}
}
}
// Composable UI
@Composable
fun UserScreen(viewModel: UserViewModel) {
val state by viewModel.state.collectAsState()
when (state) {
is UiState.Loading -> LoadingSpinner()
is UiState.Success -> UserProfile((state as UiState.Success<User>).data)
is UiState.Error -> ErrorMessage((state as UiState.Error).message)
is UiState.Empty -> EmptyState("No user found")
}
}🔄 Use Case: Result Wrapper
sealed class Result<out T> {
data class Success<T>(val value: T) : Result<T>()
data class Failure(val error: Throwable) : Result<Nothing>()
// Utility methods
inline fun onSuccess(action: (T) -> Unit): Result<T> {
if (this is Success) action(value)
return this
}
inline fun onFailure(action: (Throwable) -> Unit): Result<T> {
if (this is Failure) action(error)
return this
}
fun getOrNull(): T? = (this as? Success)?.value
fun getOrElse(default: @UnsafeVariance T): T = getOrNull() ?: default
inline fun <R> map(transform: (T) -> R): Result<R> = when (this) {
is Success -> Success(transform(value))
is Failure -> this
}
}
// Usage
fun fetchUser(id: Long): Result<User> {
return try {
val user = api.getUser(id)
Result.Success(user)
} catch (e: Exception) {
Result.Failure(e)
}
}
fun main() {
fetchUser(123)
.onSuccess { user -> println("Got user: ${user.name}") }
.onFailure { error -> println("Error: ${error.message}") }
.map { user -> user.email }
.getOrElse("[email protected]")
}🔧 Sealed Interface (Kotlin 1.5+)
sealed interface Error {
val message: String
data class NetworkError(override val message: String, val code: Int) : Error
data class DatabaseError(override val message: String, val query: String) : Error
data class ValidationError(override val message: String, val field: String) : Error
}
// Class có thể implement nhiều sealed interfaces
sealed interface Recoverable
sealed interface Loggable
data class RetryableError(
override val message: String,
val retryCount: Int
) : Error, Recoverable
fun handleError(error: Error) = when (error) {
is Error.NetworkError -> "Network issue: ${error.code}"
is Error.DatabaseError -> "DB error in query: ${error.query}"
is Error.ValidationError -> "Invalid ${error.field}"
is RetryableError -> "Will retry (${error.retryCount})"
}🛠️ Thực hành
Bài tập: Tạo Payment sealed class
// TODO: Tạo sealed class Payment với:
// - Pending(amount: Double)
// - Processing(transactionId: String)
// - Completed(transactionId: String, timestamp: Long)
// - Failed(reason: String, canRetry: Boolean)
// - Refunded(originalTransactionId: String, refundAmount: Double)Lời giải:
sealed class Payment {
abstract val amount: Double
data class Pending(override val amount: Double) : Payment()
data class Processing(
override val amount: Double,
val transactionId: String
) : Payment()
data class Completed(
override val amount: Double,
val transactionId: String,
val timestamp: Long = System.currentTimeMillis()
) : Payment()
data class Failed(
override val amount: Double,
val reason: String,
val canRetry: Boolean
) : Payment()
data class Refunded(
override val amount: Double,
val originalTransactionId: String,
val refundAmount: Double
) : Payment()
fun statusDescription(): String = when (this) {
is Pending -> "💳 Waiting for payment of $$amount"
is Processing -> "⏳ Processing transaction: $transactionId"
is Completed -> "✅ Payment completed: $transactionId"
is Failed -> "❌ Failed: $reason" + if (canRetry) " (can retry)" else ""
is Refunded -> "↩️ Refunded $$refundAmount"
}
}
fun main() {
val payments = listOf(
Payment.Pending(100.0),
Payment.Processing(100.0, "TXN-001"),
Payment.Completed(100.0, "TXN-001"),
Payment.Failed(50.0, "Insufficient funds", canRetry = true),
Payment.Refunded(75.0, "TXN-002", 75.0)
)
payments.forEach { println(it.statusDescription()) }
}⚠️ Lưu ý quan trọng
ℹ️
Kotlin 1.5+: Sealed subclasses có thể ở different files trong cùng module. Trước đó phải cùng file.
⚠️
Generic sealed class với Nothing:
sealed class Result<out T> {
data class Success<T>(val data: T) : Result<T>()
data class Error(val msg: String) : Result<Nothing>() // Nothing = works for any T
}
val intResult: Result<Int> = Result.Error("Failed") // ✅ OK
val stringResult: Result<String> = Result.Error("Failed") // ✅ OK✅ Checklist - Tự kiểm tra
Sau bài học này, bạn có thể:
- Hiểu tại sao sealed class hữu ích (exhaustive when, type safety)
- Tạo sealed class với data class, object, và class subclasses
- Sử dụng sealed class trong
whenexpression - Phân biệt sealed class vs enum
- Áp dụng UiState pattern
- Tạo Result/Either pattern với sealed class
- Sử dụng sealed interface (Kotlin 1.5+)
Tiếp theo: Inheritance
Last updated on