Kotlin Sealed classes are a powerful tool for implementing a type hierarchy with a finite set of classes. A sealed class can have several subclasses, but all of them must be defined within the same file. This restriction allows the compiler to determine all possible subclasses of a sealed class, making it possible to use exhaustive when statements to handle all possible cases.
In this blog, we’ll explore the benefits of using Kotlin sealed classes, their syntax, and real-world examples of how to use them effectively.
What are Kotlin Sealed Classes?
Sealed classes are a type of class that can only be subclassed within the same file in which it is declared. This means that all subclasses of a sealed class must be defined within the same Kotlin file. Sealed classes provide a restricted class hierarchy, which means that a sealed class can only have a finite number of subclasses.
Syntax of Kotlin Sealed Class
A sealed class is declared using the sealed
keyword, followed by the class name. Subclasses of a sealed class are defined within the same file and are marked as data
, enum
, or regular classes.
sealed class Shape {
data class Rectangle(val width: Int, val height: Int) : Shape()
data class Circle(val radius: Int) : Shape()
object Empty : Shape()
}
In the example above, we’ve declared a sealed class Shape
. It has two subclasses, Rectangle
and Circle
, which are data classes, and an object Empty
. Since these subclasses are defined in the same file as the sealed class, they are the only possible subclasses of Shape
.
Different ways to define Kotlin sealed class
In Kotlin, there are a few different ways you can define sealed classes in a Kotlin file. Here are some examples:
1. Defining a sealed class with subclasses defined in the same file
sealed class Fruit {
data class Apple(val variety: String) : Fruit()
data class Orange(val seedCount: Int) : Fruit()
object Banana : Fruit()
}
Here, we’ve defined a sealed class called Fruit with three subclasses: Apple, Orange, and Banana. The subclasses are defined in the same file as the sealed class.
2. Defining a Kotlin sealed class with subclasses defined in different files
Fruit.kt
package com.softaai.fruits
sealed class Fruit {
abstract val name: String
}
Apple.kt
package com.softaai.fruits
data class Apple(override val name: String, val variety: String) : Fruit()
Orange.kt
package com.softaai.fruits
data class Orange(override val name: String, val seedCount: Int) : Fruit()
Banana.kt
package com.softaai.fruits
object Banana : Fruit() {
override val name: String = "Banana"
}
Here in this case, we have organized all files into a package called com.softaai.fruits.
we have a Fruit
sealed class defined in a file called Fruit.kt
. The Fruit
class is abstract, meaning that it cannot be instantiated directly, and it has an abstract property called name
.
We then have three subclasses of Fruit
defined in separate files: Apple.kt
, Orange.kt
, and Banana.kt
. Each of these subclasses extends the Fruit
sealed class and provides its own implementation of the name
property.
The Apple
and Orange
subclasses are defined as data classes, which means that they automatically generate methods for creating and copying objects. The Banana
subclass is defined as an object, which means that it is a singleton and can only have one instance.
Note — When defining a sealed class and its subclasses in separate files, you will need to ensure that all of the files are included in the same module or package. This can be done by organizing the files into the same directory or package, or by including them in the same module in your build system.
By defining the subclasses in separate files, we can organize our code more effectively and make it easier to maintain. We can also import the subclasses only when we need them, which can help to reduce the size of our codebase and improve performance.
3. Defining a sealed class with subclasses defined inside a companion object
sealed class Vehicle {
companion object {
data class Car(val make: String, val model: String) : Vehicle()
data class Truck(val make: String, val model: String, val payloadCapacity: Int) : Vehicle()
object Motorcycle : Vehicle()
}
}
Here, we’ve defined a sealed class called Vehicle with three subclasses: Car, Truck, and Motorcycle. The subclasses are defined inside a companion object of the sealed class.
4. Define a sealed class in Kotlin by using an interface
interface Shape
sealed class TwoDimensionalShape : Shape {
data class Circle(val radius: Double) : TwoDimensionalShape()
data class Square(val sideLength: Double) : TwoDimensionalShape()
}
sealed class ThreeDimensionalShape : Shape {
data class Sphere(val radius: Double) : ThreeDimensionalShape()
data class Cube(val sideLength: Double) : ThreeDimensionalShape()
}
In this example, we’ve defined an interface called Shape
, which is implemented by two sealed classes: TwoDimensionalShape
and ThreeDimensionalShape
. Each sealed class has its own subclasses, representing different types of shapes.
Using an interface in this way can be useful if you want to define a common set of methods or properties that apply to all subclasses of a sealed class. In this example, we could define methods like calculateArea()
or calculateVolume()
in the Shape
interface, which could be implemented by each subclass.
How to Use Kotlin Sealed Classes?
To use sealed classes, you first need to declare a sealed class using the sealed
keyword, followed by the class name. You can then define subclasses of the sealed class within the same Kotlin file.
sealed class Shape {
class Circle(val radius: Double) : Shape()
class Rectangle(val width: Double, val height: Double) : Shape()
}
To create an instance of a subclass, you can use the val
or var
keyword followed by the name of the subclass.
val circle = Shape.Circle(5.0)
val rectangle = Shape.Rectangle(10.0, 20.0)
You can also use when expressions to perform pattern matching on a sealed class hierarchy. This can be particularly useful when you need to perform different actions based on the type of object.
fun calculateArea(shape: Shape): Double = when (shape) {
is Shape.Circle -> Math.PI * shape.radius * shape.radius
is Shape.Rectangle -> shape.width * shape.height
}
In this example, we have defined a function that takes a Shape
object as a parameter and returns the area of the shape. We then use a when expression to perform pattern matching on the Shape
object, and calculate the area based on the type of the object.
Let’s see another example of Pattern Matching using when statement
fun describeFruit(fruit: Fruit) {
when (fruit) {
is Fruit.Apple -> println("This is an ${fruit.variety} apple")
is Fruit.Orange -> println("This is an orange with ${fruit.seedCount} seeds")
is Fruit.Banana -> println("This is a banana")
}
}
In this example, we’ve defined a function called describeFruit
that takes a parameter of type Fruit
. Using a when expression, we can pattern match on the different subclasses of Fruit
and print out a description of each one.
Using sealed classes in combination with when expressions can make your code more concise and expressive, and can help you avoid complex if-else or switch statements. It’s a powerful feature of Kotlin that can make your code easier to read and maintain.
Real-world Examples of Kotlin Sealed Class
Here are some examples of how sealed classes are used in real-time Android applications.
- Result Type: A sealed class can be used to represent the possible outcomes of a computation, such as Success or Failure.
sealed class Result<out T : Any> {
data class Success<out T : Any>(val data: T) : Result<T>()
data class Failure(val error: String) : Result<Nothing>()
}
2. Network Responses: Sealed classes can be used to represent the different types of network responses, including successful responses, error responses, and loading states.
sealed class NetworkResponse<out T : Any> {
data class Success<out T : Any>(val data: T) : NetworkResponse<T>()
data class Error(val errorMessage: String) : NetworkResponse<Nothing>()
object Loading : NetworkResponse<Nothing>()
}
3. Event Type: A sealed class can be used to represent the possible types of events that can occur in an application, such as UserClick or NetworkError.
sealed class Event {
object UserClick : Event()
object NetworkError : Event()
data class DataLoaded(val data: List<String>) : Event()
}
4. Navigation: Sealed classes can be used to represent the different types of navigation events, such as navigating to a new screen or showing a dialog.
sealed class NavigationEvent {
data class NavigateToScreen(val screenName: String) : NavigationEvent()
data class ShowDialog(val dialogId: String) : NavigationEvent()
}
In this example, we have declared a sealed class named NavigationEvent
that has two subclasses: NavigateToScreen
and ShowDialog
. The NavigateToScreen
subclass contains the name of the screen to navigate to, and the ShowDialog
subclass contains the ID of the dialog to show.
We can then use this sealed class to handle navigation events:
fun handleNavigationEvent(event: NavigationEvent) {
when (event) {
is NavigationEvent.NavigateToScreen -> {
// navigate to new screen
val screenName = event.screenName
}
is NavigationEvent.ShowDialog -> {
// show dialog
val dialogId = event.dialogId
}
}
}
In both of these examples, sealed classes provide a type-safe way to handle different types of events.
Properties of Kotlin Sealed Class
- Limited Subclasses: A sealed class can only have a limited set of subclasses. This restriction makes it easier to handle all possible cases of a sealed class in a when statement.
- Inheritance: Subclasses of a sealed class can inherit from the sealed class or from other subclasses. This allows for a flexible and modular class hierarchy.
- Constructor Parameters: Subclasses of a sealed class can have their own set of constructor parameters. This allows for more fine-grained control over the properties of each subclass.
Limitations of Kotlin Sealed Class
- File Scope: All subclasses of a sealed class must be defined in the same file as the sealed class. This can be limiting if you want to define subclasses in separate files or modules.
- Singleton Objects: A sealed class can have a singleton object as a subclass, but this object cannot have any parameters. This can be limiting if you need to define a singleton object with specific properties.
Advantages of Kotlin Sealed Class
- Type safety: Sealed classes provide type safety by restricting the set of classes that can be used in a particular context. This makes the code more robust and less prone to errors.
- Extensibility: Sealed classes can be easily extended by adding new subclasses to the hierarchy. This allows you to add new functionality to your code without affecting existing code.
- Pattern matching: Sealed classes can be used with pattern matching to handle different cases based on the type of the object. This makes it easy to write clean and concise code.
- Flexibility: Sealed classes can have their own state and behavior, which makes them more flexible than enums or other data types that are used to represent a finite set of related classes.
Sealed Classes vs Enum Classes
Enums and sealed classes are both used to define a restricted set of values, but they have some differences in their implementation and usage.
Here are some key differences between enums and sealed classes:
- Inheritance: Enums are not designed for inheritance. All of the values of an enum are defined at the same level, and they cannot be extended or inherited from.
Sealed classes, on the other hand, are designed for inheritance. A sealed class can have multiple subclasses, and these subclasses can be defined in separate files or packages. Each subclass can have its own properties, methods, and behavior, and can be used to represent a more specific type or subtype of the sealed class.
Here is an example of an enum and a kotlin sealed class that represent different types of fruits:
// Using an enum
enum class FruitEnum {
APPLE,
ORANGE,
BANANA
}
// Using a sealed class
sealed class FruitSealed {
object Apple : FruitSealed()
object Orange : FruitSealed()
object Banana : FruitSealed()
}
In this example, the FruitEnum
has three values, each representing a different type of fruit. The FruitSealed
class also has three values, but each one is defined as a separate object and inherits from the sealed class.
2. Extensibility: Enums are not very extensible. Once an enum is defined, it cannot be extended or modified.
Sealed classes are more extensible. New subclasses can be added to a sealed class at any time, as long as they are defined within the same file or package. This allows for more flexibility in the types of values that can be represented by the sealed class.
Here is an example of how a sealed class can be extended with new subclasses:
sealed class FruitSealed {
object Apple : FruitSealed()
object Orange : FruitSealed()
object Banana : FruitSealed()
}
object Pineapple : FruitSealed()
3. Functionality: Enums can have some basic functionality, such as properties and methods, but they are limited in their ability to represent more complex data structures or behaviors.
Kotlin Sealed classes can have much more complex functionality, including properties, methods, and behavior specific to each subclass. This makes them useful for representing more complex data structures or modeling inheritance relationships between types.
Here is an example of how a kotlin sealed class can be used to represent a hierarchy of different types of animals:
sealed class Animal {
abstract val name: String
abstract fun makeSound()
}
data class Dog(override val name: String) : Animal() {
override fun makeSound() {
println("Woof!")
}
}
data class Cat(override val name: String) : Animal() {
override fun makeSound() {
println("Meow!")
}
}
sealed class WildAnimal : Animal()
data class Lion(override val name: String) : WildAnimal() {
override fun makeSound() {
println("Roar!")
}
}
data class Elephant(override val name: String) : WildAnimal() {
override fun makeSound() {
println("Trumpet!")
}
}
In this example, we have a sealed class called Animal
with two subclasses, Dog
and Cat
. Each subclass has a name
property and a makeSound()
method that prints out the sound the animal makes.
We have also defined a second sealed class called WildAnimal
, which extends Animal
. WildAnimal
has two subclasses, Lion
and Elephant
, which also have name
and makeSound()
methods. Because WildAnimal
extends Animal
, it inherits all of the properties and methods of Animal
.
With this hierarchy of classes, we can represent different types of animals and their behaviors. We can create instances of Dog
and Cat
to represent domestic animals, and instances of Lion
and Elephant
to represent wild animals.
val dog = Dog("Rufus")
dog.makeSound() // Output: Woof!
val lion = Lion("Simba")
lion.makeSound() // Output: Roar!
In short, enums and sealed classes are both useful for defining restricted sets of values, but they have some key differences in their implementation and usage. Enums are simple and easy to use, but sealed classes are more flexible and powerful, making them a good choice for modeling more complex data structures or inheritance relationships.
Summary
In summary, Kotlin sealed classes are a powerful feature of Kotlin that provide type safety, extensibility, pattern matching, and flexibility. They are used to represent a closed hierarchy of related classes that share some common functionality or properties, and are a useful alternative to enums. However, it’s important to consider the requirements of your code and choose the best data type for your specific use case.