Kotlin - Sealed Classes

Sealed Classes in Kotlin

Kotlin, the modern and expressive programming language developed by JetBrains, introduces many advanced concepts that aim to simplify code while increasing safety and readability. One such powerful feature is the sealed class. Sealed classes are a core part of Kotlin's type system and are especially useful when working with restricted class hierarchies and representing state or result types. They are primarily used to model finite state machines, represent algebraic data types, or control flow branches that are exhaustive at compile time.

What is a Sealed Class?

A sealed class is a special kind of class in Kotlin that restricts which other classes may inherit from it. In simpler terms, it allows you to define a closed set of subclasses in the same file where the sealed class is declared. This means all the possible subclasses of the sealed class are known at compile time, which makes them particularly valuable in when expressions.


sealed class Result
data class Success(val data: String) : Result()
data class Error(val exception: Throwable) : Result()
object Loading : Result()
    

In the example above, Result is a sealed class with three possible types: Success, Error, and Loading. This structure allows exhaustive when expressions and better control over states.

Key Characteristics of Sealed Classes

  • Sealed classes are implicitly abstract and cannot be instantiated directly.
  • All subclasses must be declared in the same Kotlin file as the sealed class itself.
  • Subclasses can be either data classes, object declarations, or regular classes.
  • They improve type-safety by enabling exhaustive when checks.

Syntax


sealed class Operation

class Add(val value: Int) : Operation()
class Subtract(val value: Int) : Operation()
    

In this example, the Operation class is sealed and can only be extended within the same file.

Why Use Sealed Classes?

Sealed classes provide a safe and structured way to represent limited class hierarchies. They are ideal for modeling:

  • Result types (e.g., success, error, loading)
  • UI states (e.g., Empty, Loading, Success, Failure)
  • Events or commands in MVI architecture
  • Finite state machines

Sealed Classes vs Enums

Aspect Sealed Classes Enums
Can hold state/data Yes Limited
Extensibility Flexible (custom behavior per subclass) Limited to predefined constants
Inheritance Supports subclassing No inheritance allowed
Use Case Complex states or types with data Fixed constant values

Using Sealed Classes with when Expression

One of the most compelling features of sealed classes is their integration with when expressions, allowing the compiler to verify that all cases are covered.


fun handleResult(result: Result): String {
    return when (result) {
        is Success -> "Data: ${result.data}"
        is Error -> "Error: ${result.exception.message}"
        Loading -> "Loading..."
    }
}
    

Notice that we didn’t need an else branch. Kotlin knows all possible types of Result and ensures exhaustiveness.

Sealed Interfaces

Kotlin 1.5 introduced sealed interfaces, expanding the utility of sealed types to interfaces as well.


sealed interface NetworkState

data class Connected(val speed: Int): NetworkState
object Disconnected : NetworkState
    

Sealed interfaces behave similarly to sealed classes but provide more flexibility for modeling behaviors.

Sealed Classes with Data Classes

A common pattern is combining sealed classes with data classes to represent variations in state or results:


sealed class ApiResponse

data class Success(val response: String) : ApiResponse()
data class Failure(val code: Int, val message: String) : ApiResponse()
object Timeout : ApiResponse()
    

This makes it simple to pass around structured results without creating many unrelated classes.

Nested Sealed Classes

You can nest sealed classes within other sealed or non-sealed classes:


class NetworkRequest {
    sealed class Status {
        object Loading : Status()
        data class Success(val data: String) : Status()
        data class Error(val error: Throwable) : Status()
    }
}
    

This is especially useful when encapsulating logic within a class or module.

Companion Objects and Sealed Classes

You can use companion objects inside sealed classes to define factory methods or constants:


sealed class PaymentMethod {
    data class CreditCard(val number: String): PaymentMethod()
    object Cash: PaymentMethod()

    companion object {
        fun defaultMethod(): PaymentMethod = Cash
    }
}
    

Sealed Class Use Cases in Android

  • State Management: Representing ViewState in MVVM or MVI architectures.
  • Navigation Events: Representing navigation commands between fragments or activities.
  • UI Actions: Modeling user actions or intents.
  • API Responses: Wrapping responses in Success/Error/Loading types for LiveData or StateFlow.

Sealed Class vs Abstract Class

While both allow defining a common type with subclasses, sealed classes provide better compiler support:

  • Sealed classes enforce subclass declarations in the same file.
  • Sealed classes improve safety with exhaustive when checks.
  • Abstract classes do not restrict subclass declaration location.

Best Practices

  • Use sealed classes for modeling finite, known-at-compile-time hierarchies.
  • Prefer data classes for subclasses to hold immutable state.
  • Use object declarations for singletons or constant-like values.
  • Keep all subclasses in the same file for maintainability and clarity.

Sealed Class Limitations

  • All subclasses must be declared in the same file as the sealed class.
  • You cannot create instances of sealed classes directly.
  • Cannot be instantiated without a known subclass.

Real-World Example: Authentication State


sealed class AuthState {
    object Unauthenticated : AuthState()
    object Loading : AuthState()
    data class Authenticated(val userId: String) : AuthState()
    data class Error(val message: String) : AuthState()
}

fun render(authState: AuthState) {
    when (authState) {
        is AuthState.Loading -> println("Logging in...")
        is AuthState.Authenticated -> println("Welcome, ${authState.userId}")
        is AuthState.Unauthenticated -> println("Please login.")
        is AuthState.Error -> println("Error: ${authState.message}")
    }
}
    

This structure gives clarity and type safety in managing user authentication states in a frontend or backend system.

Sealed classes in Kotlin offer a robust and elegant way to handle type-safe and exhaustive class hierarchies. They allow developers to represent restricted class hierarchies in a readable and maintainable manner. When used correctly, sealed classes can greatly simplify control flow and reduce the possibility of unhandled cases, especially when used in conjunction with when expressions.

Whether you're building Android applications, backend services, or library components, sealed classes are a fundamental feature of Kotlin that can lead to more expressive and safer codebases.

Beginner 5 Hours

Sealed Classes in Kotlin

Kotlin, the modern and expressive programming language developed by JetBrains, introduces many advanced concepts that aim to simplify code while increasing safety and readability. One such powerful feature is the sealed class. Sealed classes are a core part of Kotlin's type system and are especially useful when working with restricted class hierarchies and representing state or result types. They are primarily used to model finite state machines, represent algebraic data types, or control flow branches that are exhaustive at compile time.

What is a Sealed Class?

A sealed class is a special kind of class in Kotlin that restricts which other classes may inherit from it. In simpler terms, it allows you to define a closed set of subclasses in the same file where the sealed class is declared. This means all the possible subclasses of the sealed class are known at compile time, which makes them particularly valuable in when expressions.

sealed class Result data class Success(val data: String) : Result() data class Error(val exception: Throwable) : Result() object Loading : Result()

In the example above, Result is a sealed class with three possible types: Success, Error, and Loading. This structure allows exhaustive when expressions and better control over states.

Key Characteristics of Sealed Classes

  • Sealed classes are implicitly abstract and cannot be instantiated directly.
  • All subclasses must be declared in the same Kotlin file as the sealed class itself.
  • Subclasses can be either data classes, object declarations, or regular classes.
  • They improve type-safety by enabling exhaustive when checks.

Syntax

sealed class Operation class Add(val value: Int) : Operation() class Subtract(val value: Int) : Operation()

In this example, the Operation class is sealed and can only be extended within the same file.

Why Use Sealed Classes?

Sealed classes provide a safe and structured way to represent limited class hierarchies. They are ideal for modeling:

  • Result types (e.g., success, error, loading)
  • UI states (e.g., Empty, Loading, Success, Failure)
  • Events or commands in MVI architecture
  • Finite state machines

Sealed Classes vs Enums

Aspect Sealed Classes Enums
Can hold state/data Yes Limited
Extensibility Flexible (custom behavior per subclass) Limited to predefined constants
Inheritance Supports subclassing No inheritance allowed
Use Case Complex states or types with data Fixed constant values

Using Sealed Classes with when Expression

One of the most compelling features of sealed classes is their integration with when expressions, allowing the compiler to verify that all cases are covered.

fun handleResult(result: Result): String { return when (result) { is Success -> "Data: ${result.data}" is Error -> "Error: ${result.exception.message}" Loading -> "Loading..." } }

Notice that we didn’t need an else branch. Kotlin knows all possible types of Result and ensures exhaustiveness.

Sealed Interfaces

Kotlin 1.5 introduced sealed interfaces, expanding the utility of sealed types to interfaces as well.

sealed interface NetworkState data class Connected(val speed: Int): NetworkState object Disconnected : NetworkState

Sealed interfaces behave similarly to sealed classes but provide more flexibility for modeling behaviors.

Sealed Classes with Data Classes

A common pattern is combining sealed classes with data classes to represent variations in state or results:

sealed class ApiResponse data class Success(val response: String) : ApiResponse() data class Failure(val code: Int, val message: String) : ApiResponse() object Timeout : ApiResponse()

This makes it simple to pass around structured results without creating many unrelated classes.

Nested Sealed Classes

You can nest sealed classes within other sealed or non-sealed classes:

class NetworkRequest { sealed class Status { object Loading : Status() data class Success(val data: String) : Status() data class Error(val error: Throwable) : Status() } }

This is especially useful when encapsulating logic within a class or module.

Companion Objects and Sealed Classes

You can use companion objects inside sealed classes to define factory methods or constants:

sealed class PaymentMethod { data class CreditCard(val number: String): PaymentMethod() object Cash: PaymentMethod() companion object { fun defaultMethod(): PaymentMethod = Cash } }

Sealed Class Use Cases in Android

  • State Management: Representing ViewState in MVVM or MVI architectures.
  • Navigation Events: Representing navigation commands between fragments or activities.
  • UI Actions: Modeling user actions or intents.
  • API Responses: Wrapping responses in Success/Error/Loading types for LiveData or StateFlow.

Sealed Class vs Abstract Class

While both allow defining a common type with subclasses, sealed classes provide better compiler support:

  • Sealed classes enforce subclass declarations in the same file.
  • Sealed classes improve safety with exhaustive when checks.
  • Abstract classes do not restrict subclass declaration location.

Best Practices

  • Use sealed classes for modeling finite, known-at-compile-time hierarchies.
  • Prefer data classes for subclasses to hold immutable state.
  • Use object declarations for singletons or constant-like values.
  • Keep all subclasses in the same file for maintainability and clarity.

Sealed Class Limitations

  • All subclasses must be declared in the same file as the sealed class.
  • You cannot create instances of sealed classes directly.
  • Cannot be instantiated without a known subclass.

Real-World Example: Authentication State

sealed class AuthState { object Unauthenticated : AuthState() object Loading : AuthState() data class Authenticated(val userId: String) : AuthState() data class Error(val message: String) : AuthState() } fun render(authState: AuthState) { when (authState) { is AuthState.Loading -> println("Logging in...") is AuthState.Authenticated -> println("Welcome, ${authState.userId}") is AuthState.Unauthenticated -> println("Please login.") is AuthState.Error -> println("Error: ${authState.message}") } }

This structure gives clarity and type safety in managing user authentication states in a frontend or backend system.

Sealed classes in Kotlin offer a robust and elegant way to handle type-safe and exhaustive class hierarchies. They allow developers to represent restricted class hierarchies in a readable and maintainable manner. When used correctly, sealed classes can greatly simplify control flow and reduce the possibility of unhandled cases, especially when used in conjunction with when expressions.

Whether you're building Android applications, backend services, or library components, sealed classes are a fundamental feature of Kotlin that can lead to more expressive and safer codebases.

Related Tutorials

Frequently Asked Questions for Kotlin

Companion objects hold static members, like Java’s static methods, in Kotlin classes.

A concise way to define anonymous functions using { parameters -> body } syntax.

Kotlin prevents null pointer exceptions using nullable (?) and non-null (!!) type syntax.

Inline functions reduce overhead by inserting function code directly at call site.

JetBrains, the makers of IntelliJ IDEA, developed Kotlin and released it in 2011.

Allows non-null variables to be initialized after declaration (used with var only).

val is immutable (read-only), var is mutable (can change value).

Compiler automatically determines variable types, reducing boilerplate code.

A data class automatically provides equals(), hashCode(), toString(), and copy() methods.

A function that takes functions as parameters or returns them.

Kotlin is a modern, statically typed language that runs on the Java Virtual Machine (JVM).

They add new methods to existing classes without modifying their source code.

It allows unpacking data class properties into separate variables.

== checks value equality; === checks reference (memory) equality.


apply is a scope function to configure an object and return it.

A class that restricts subclassing, useful for representing restricted class hierarchies.

Coroutines enable asynchronous programming by suspending and resuming tasks efficiently.

Functions can define default values for parameters, avoiding overloads.

Kotlin offers concise syntax, null safety, and modern features not found in Java.

Kotlin automatically casts variables to appropriate types after type checks.

Use the object keyword to create a singleton.

Calls a method only if the object is non-null.

Yes, Kotlin supports backend development using frameworks like Ktor and Spring Boot.

Data structures like List, Set, and Map, supporting functional operations.

line

Copyrights © 2024 letsupdateskills All rights reserved