Kotlin Sealed Classes

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.

What Are Kotlin Sealed Classes?

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.

Key Properties of Sealed 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.

Sealed Class vs Regular Class vs Enum

Understanding the differences between these class types helps you choose the right tool for your specific use case:

Sealed Class Properties

  • ✅ Can hold different data types
  • ✅ Subclasses can have multiple instances
  • ✅ Supports inheritance
  • ✅ Type-safe with exhaustive when expressions
  • ❌ Cannot be instantiated directly

Enum Properties

  • ✅ Predefined constants
  • ✅ Simple state representation
  • ❌ All constants must be same type
  • ❌ Single instance per constant
  • ❌ Limited extensibility

Regular Class Properties

  • ✅ Can be instantiated
  • ✅ Flexible inheritance
  • ❌ No compile-time guarantees for subclasses
  • ❌ Requires else clause in when expressions

Basic Sealed Class Syntax

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()  
}  

Subclass Types

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()  

Advanced Sealed Class Features

Generic Sealed Classes

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>()  
}  

Nested Sealed Classes

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()  
    }  
}  

Properties in Sealed Classes

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  
    }  
}  

Sealed Interfaces: The Modern Alternative

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  

Sealed Interface vs Sealed Class

When to Use Sealed Interfaces:

  • Multiple inheritance scenarios
  • Contract-based design
  • When you don’t need shared state
  • API design for libraries

When to Use Sealed Classes:

  • Shared properties across subclasses
  • Protected/private members needed
  • Single inheritance hierarchy
  • Complex state management

Real-World Android Implementation Examples

Example 1: Network State Management

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)  
        }  
    }  
}  

Example 2: UI State Management in Compose

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()  
        }  
    }  
}  

Example 3: Form Validation State

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)  
        }  
    }  
}  

Example 4: Navigation State Management

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)  
    }  
}  

Exhaustive When Expressions

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  
}  

Smart Casting Benefits

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()  
        }  
    }  
}  

Complete Working Example: User Authentication Flow

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.