Kotlin sealed classes are special types of classes that restrict their inheritance hierarchy to a predefined set of subclasses, all known at compile time. Unlike regular classes or enums, sealed classes provide controlled inheritance while maintaining the flexibility to hold different types of data and state information.
A sealed class in Kotlin is a powerful feature that allows you to define a restricted class hierarchy where all possible subclasses are known at compile time. Think of sealed classes as “enums with superpowers” - they combine the safety of enums with the flexibility of regular classes.
Abstract Nature: Sealed classes are implicitly abstract, meaning you cannot instantiate them directly. They serve as base classes for their subclasses.
sealed class PaymentStatus
// PaymentStatus() // ❌ Compilation error - cannot instantiate
Compile-Time Safety: All subclasses must be declared within the same module and package, ensuring the compiler knows every possible type.
Flexible Data Holding: Unlike enums, sealed class subclasses can hold different types of data and maintain state.
Exhaustive When Expressions: The compiler can verify that all possible cases are handled in when expressions.
Understanding the differences between these class types helps you choose the right tool for your specific use case:
Creating a sealed class follows a straightforward pattern:
sealed class NetworkResult {
data class Success(val data: String) : NetworkResult()
data class Error(val exception: Exception) : NetworkResult()
object Loading : NetworkResult()
}
Sealed classes support three types of subclasses:
Data Classes: Perfect for holding structured data
data class UserProfile(val name: String, val email: String) : NetworkResult()
Regular Classes: When you need custom behavior
class CustomResponse(val message: String) : NetworkResult() {
fun formatMessage(): String = "Response: $message"
}
Object Declarations: For singleton states
object Empty : NetworkResult()
Sealed classes support generics, making them incredibly versatile for different data types:
sealed class ApiResponse<out T> {
data class Success<T>(val data: T) : ApiResponse<T>()
data class Error(val errorMessage: String) : ApiResponse<Nothing>()
object Loading : ApiResponse<Nothing>()
}
You can nest sealed classes for complex hierarchies:
sealed class UIEvent {
sealed class UserAction : UIEvent() {
object Login : UserAction()
object Logout : UserAction()
data class Navigate(val route: String) : UserAction()
}
sealed class SystemEvent : UIEvent() {
object NetworkConnected : SystemEvent()
object NetworkDisconnected : SystemEvent()
}
}
Sealed classes can contain properties and methods:
sealed class Vehicle(val wheels: Int) {
abstract val maxSpeed: Int
class Car(val brand: String) : Vehicle(4) {
override val maxSpeed = 200
}
class Motorcycle(val engineSize: Int) : Vehicle(2) {
override val maxSpeed = 180
}
object Bicycle : Vehicle(2) {
override val maxSpeed = 50
}
}
Kotlin 1.5 introduced sealed interfaces, providing even more flexibility:
sealed interface MediaContent
data class VideoContent(
val url: String,
val duration: Int
) : MediaContent
data class AudioContent(
val url: String,
val bitrate: Int
) : MediaContent
data class ImageContent(
val url: String,
val resolution: String
) : MediaContent
When to Use Sealed Interfaces:
When to Use Sealed Classes:
sealed class NetworkState {
object Idle : NetworkState()
object Loading : NetworkState()
data class Success<T>(val data: T) : NetworkState()
data class Error(val exception: Throwable) : NetworkState()
}
class ApiRepository {
private val _networkState = MutableLiveData<NetworkState>()
val networkState: LiveData<NetworkState> = _networkState
suspend fun fetchUserData(userId: String) {
_networkState.value = NetworkState.Loading
try {
val userData = apiService.getUser(userId)
_networkState.value = NetworkState.Success(userData)
} catch (e: Exception) {
_networkState.value = NetworkState.Error(e)
}
}
}
sealed class ScreenState {
object Loading : ScreenState()
data class Content(val items: List<String>) : ScreenState()
data class Error(val message: String) : ScreenState()
object Empty : ScreenState()
}
@Composable
fun ContentScreen(viewModel: ContentViewModel) {
val screenState by viewModel.screenState.collectAsState()
when (screenState) {
is ScreenState.Loading -> {
CircularProgressIndicator()
}
is ScreenState.Content -> {
LazyColumn {
items(screenState.items) { item ->
Text(text = item)
}
}
}
is ScreenState.Error -> {
ErrorMessage(message = screenState.message)
}
is ScreenState.Empty -> {
EmptyStateMessage()
}
}
}
sealed class ValidationResult {
object Valid : ValidationResult()
data class Invalid(val errors: List<String>) : ValidationResult()
object Pending : ValidationResult()
}
class FormValidator {
fun validateEmail(email: String): ValidationResult {
val errors = mutableListOf<String>()
if (email.isBlank()) {
errors.add("Email cannot be empty")
}
if (!email.contains("@")) {
errors.add("Invalid email format")
}
return if (errors.isEmpty()) {
ValidationResult.Valid
} else {
ValidationResult.Invalid(errors)
}
}
}
sealed class NavigationEvent {
object NavigateBack : NavigationEvent()
data class NavigateToScreen(val route: String) : NavigationEvent()
data class NavigateWithData(val route: String, val data: Bundle) : NavigationEvent()
object ClearBackStack : NavigationEvent()
}
class NavigationManager {
private val _navigationEvents = MutableSharedFlow<NavigationEvent>()
val navigationEvents = _navigationEvents.asSharedFlow()
fun navigateToProfile(userId: String) {
val bundle = Bundle().apply {
putString("userId", userId)
}
_navigationEvents.tryEmit(
NavigationEvent.NavigateWithData("profile", bundle)
)
}
fun goBack() {
_navigationEvents.tryEmit(NavigationEvent.NavigateBack)
}
}
One of the most powerful features of sealed classes is exhaustive when expressions:
fun handleNetworkState(state: NetworkState): String {
return when (state) {
is NetworkState.Idle -> "Ready to make request"
is NetworkState.Loading -> "Loading data..."
is NetworkState.Success -> "Data loaded: ${state.data}"
is NetworkState.Error -> "Error occurred: ${state.exception.message}"
}
// No else clause needed - compiler ensures all cases are covered
}
Kotlin automatically smart-casts sealed class instances within when expressions:
fun processApiResponse(response: ApiResponse<User>) {
when (response) {
is ApiResponse.Success -> {
// response is automatically cast to ApiResponse.Success<User>
val user = response.data // Direct access to data
updateUserProfile(user)
}
is ApiResponse.Error -> {
// response is automatically cast to ApiResponse.Error
showErrorMessage(response.errorMessage)
}
ApiResponse.Loading -> {
showLoadingIndicator()
}
}
}
Here’s a comprehensive example demonstrating sealed classes in a real Android authentication system:
// Import statements
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import retrofit2.HttpException
import java.io.IOException
// Sealed class for authentication states
sealed class AuthState {
object Idle : AuthState()
object Loading : AuthState()
data class Success(val user: User, val token: String) : AuthState()
data class Error(val message: String, val errorCode: Int? = null) : AuthState()
}
// User data class
data class User(
val id: String,
val username: String,
val email: String,
val profileImage: String?
)
// Authentication service interface
interface AuthService {
suspend fun login(username: String, password: String): LoginResponse
suspend fun logout(token: String): Boolean
}
// Response data class
data class LoginResponse(
val user: User,
val token: String,
val expiresIn: Long
)
// ViewModel implementing authentication flow
class AuthViewModel(
private val authService: AuthService
) : ViewModel() {
private val _authState = MutableStateFlow<AuthState>(AuthState.Idle)
val authState: StateFlow<AuthState> = _authState
fun login(username: String, password: String) {
viewModelScope.launch {
_authState.value = AuthState.Loading
try {
val response = authService.login(username, password)
_authState.value = AuthState.Success(
user = response.user,
token = response.token
)
} catch (e: HttpException) {
_authState.value = AuthState.Error(
message = "Login failed: ${e.message()}",
errorCode = e.code()
)
} catch (e: IOException) {
_authState.value = AuthState.Error(
message = "Network error: Check your connection"
)
} catch (e: Exception) {
_authState.value = AuthState.Error(
message = "Unexpected error: ${e.localizedMessage}"
)
}
}
}
fun logout() {
_authState.value = AuthState.Idle
}
fun clearError() {
if (_authState.value is AuthState.Error) {
_authState.value = AuthState.Idle
}
}
}
// Activity/Fragment implementation
class LoginActivity : AppCompatActivity() {
private lateinit var authViewModel: AuthViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_login)
// Initialize ViewModel
authViewModel = AuthViewModel(authService)
// Observe authentication state
observeAuthState()
// Setup UI click listeners
setupClickListeners()
}
private fun observeAuthState() {
lifecycleScope.launch {
authViewModel.authState.collect { state ->
handleAuthState(state)
}
}
}
private fun handleAuthState(state: AuthState) {
when (state) {
is AuthState.Idle -> {
hideLoading()
clearErrors()
enableLoginButton(true)
}
is AuthState.Loading -> {
showLoading()
enableLoginButton(false)
clearErrors()
}
is AuthState.Success -> {
hideLoading()
clearErrors()
// Navigate to main screen
navigateToMainScreen(state.user)
finish()
}
is AuthState.Error -> {
hideLoading()
enableLoginButton(true)
showError(state.message)
// Handle specific error codes
when (state.errorCode) {
401 -> highlightInvalidCredentials()
429 -> showRateLimitWarning()
500 -> showServerErrorMessage()
}
}
}
}
private fun setupClickListeners() {
loginButton.setOnClickListener {
val username = usernameEditText.text.toString().trim()
val password = passwordEditText.text.toString()
if (validateInput(username, password)) {
authViewModel.login(username, password)
}
}
retryButton.setOnClickListener {
authViewModel.clearError()
}
}
private fun validateInput(username: String, password: String): Boolean {
return when {
username.isEmpty() -> {
usernameEditText.error = "Username cannot be empty"
false
}
password.isEmpty() -> {
passwordEditText.error = "Password cannot be empty"
false
}
password.length < 6 -> {
passwordEditText.error = "Password must be at least 6 characters"
false
}
else -> true
}
}
private fun showLoading() {
progressBar.visibility = View.VISIBLE
}
private fun hideLoading() {
progressBar.visibility = View.GONE
}
private fun showError(message: String) {
errorTextView.text = message
errorTextView.visibility = View.VISIBLE
}
private fun clearErrors() {
errorTextView.visibility = View.GONE
usernameEditText.error = null
passwordEditText.error = null
}
private fun enableLoginButton(enabled: Boolean) {
loginButton.isEnabled = enabled
loginButton.alpha = if (enabled) 1.0f else 0.6f
}
private fun navigateToMainScreen(user: User) {
val intent = Intent(this, MainActivity::class.java).apply {
putExtra("user_id", user.id)
putExtra("username", user.username)
}
startActivity(intent)
}
private fun highlightInvalidCredentials() {
usernameEditText.error = "Invalid credentials"
passwordEditText.error = "Invalid credentials"
}
private fun showRateLimitWarning() {
Toast.makeText(
this,
"Too many login attempts. Please try again later.",
Toast.LENGTH_LONG
).show()
}
private fun showServerErrorMessage() {
Toast.makeText(
this,
"Server is currently unavailable. Please try again later.",
Toast.LENGTH_LONG
).show()
}
}
// Extension function for easier state checking
fun AuthState.isLoading(): Boolean = this is AuthState.Loading
fun AuthState.isSuccess(): Boolean = this is AuthState.Success
fun AuthState.isError(): Boolean = this is AuthState.Error
// Usage in other components
class NetworkStateIndicator : View {
fun updateNetworkStatus(authState: AuthState) {
when {
authState.isLoading() -> showLoadingIndicator()
authState.isError() -> showErrorIndicator()
authState.isSuccess() -> showSuccessIndicator()
}
}
}
Output when running this code:
Initial State: Idle
User clicks login -> Loading state (progress bar shows)
Network request success -> Success state (navigate to main screen)
OR
Network request failure -> Error state (show error message)
This comprehensive example demonstrates how Kotlin sealed classes provide type-safe, maintainable, and scalable state management for Android applications. The sealed class ensures compile-time safety while offering the flexibility to handle complex authentication flows with different data types and error scenarios.