Skip to Content

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

FeatureSealed ClassEnum
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 typesclass, data class, objectChỉ constants
Exhaustive when✅ Có✅ Có
Use caseComplex states với dataSimple 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 when expression
  • 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