We can use enumerations to implement such models, but enumerations themselves have many limitations. Each value of an enumeration type is only allowed one instance, and enumerations cannot add additional information for each type, for example, you cannot add data of type Exception associated with “Error” in the enumeration.

Of course, you could take an abstract class and have some classes inherit from it, so you could extend it at will, but that would lose the finite collection advantage of enumerations. Sealed Class (sealed Class) contains the advantages of both — the flexibility of the abstract class representation and the limitations of the set in the enumeration. Read on to learn more about seals, or check out the video below:

  • Tencent Video link:

V.qq.com/x/page/b094…

  • Bilibili video link:

www.bilibili.com/video/BV1Nk…

Basic use of sealing class

Like abstract classes, sealed classes can be used to represent hierarchical relationships. A subclass can be any class: a data class, a Kotlin object, a normal class, or even another sealed class. But unlike abstract classes, you must declare the hierarchy in the same file or nested inside the class.

// Result.kt
sealed class Result<out T : Any> {
   data class Success<out T : Any>(val data: T) : Result<T>()
   data class Error(val exception: Exception) : Result<Nothing>()
}
Copy the code

Attempts to inherit a class outside the file defined by the sealed class (external inheritance) result in a compilation error:

Cannot access '<init>' : it is privatein Result
Copy the code

Forgot a branch?

In the WHEN statement, we often need to deal with all possible types:


    when(result) {
        is Result.Success -> { }
        is Result.Error -> { }
    }
Copy the code

But if someone adds a new type to the Result class: InProgress:

sealed class Result<out T : Any> { 
 
   data class Success<out T : Any>(val data: T) : Result<T>()
   data class Error(val exception: Exception) : Result<Nothing>()
   object InProgress : Result<Nothing>()
}
Copy the code

If we want to prevent missing processing of new types, we don’t have to rely on memorizing them ourselves or using the IDE’s search function to confirm the newly added type. When we use the WHEN statement to process sealed classes, we can ask the compiler to give us an error if we don’t override everything. Like the if statement, when used as an expression, the compiler issues an error to enforce that all options must be overridden (that is, exhausted):


val action = when(result) {
  is Result.Success -> { }
  is Result.Error -> { }
}
Copy the code

Add “is inProgress” or “else” branches when the expression must override all options.

If you want to get the same compiler prompt when using the WHEN statement, you can add the following extended attributes:

val <T> T.exhaustive: T
    get() = this
Copy the code

This way, simply add “.exhaustive” to the when statement, and the compiler will return the same error if any branch is not covered.


when(result){
    is Result.Success -> { }
    is Result.Error -> { }
}.exhaustive
Copy the code

IDE auto completion

Since all subtypes of a sealed class are known, the IDE can help us complete all branches under the WHEN statement:

sealed class Result<out T : Any> {
  data class Success<out T : Any>(val data: T) : Result<T>()
  sealed class Error(val exception: Exception) : Result<Nothing>() {
     class RecoverableError(exception: Exception) : Error(exception)
     class NonRecoverableError(exception: Exception) : Error(exception)
  }
    object InProgress : Result<Nothing>()
}
Copy the code

The working principle of

Why do sealed classes have these features? Let’s see what the decompiled Java code does:


sealed class Result
data class Success(val data: Any) : Result()
data class Error(val exception: Exception) : Result()
 
@Metadata(
   ...
   d2 = {"Lio/testapp/Result;"."T".""."()V"."Error"."Success"."Lio/testapp/Result$Success;"."Lio/testapp/Result$Error;". } ) public abstract class Result { privateResult() {
   }
 
   // $FF: synthetic method
   public Result(DefaultConstructorMarker $constructor_marker) { this(); }}Copy the code

The metadata of the sealed class keeps a list of subclasses, and the compiler can use this information where needed.

Result is an abstract class and contains two constructors:

  • A private default constructor
  • A synthetic constructor that only the Kotlin compiler can use

This means that no other class can call the constructor of the sealed class directly. If we look at the decompiled code for the Success class, we can see that it calls the synthesized constructor of Result:

public final class Success extends Result {
   @NotNull
   private final Object data
 
   public Success(@NotNull Object data) {
      Intrinsics.checkParameterIsNotNull(data, "data");
      super((DefaultConstructorMarker)null);
      this.data = data;
   }
Copy the code

Start using sealed classes to limit class hierarchies, and let the compiler and IDE help avoid type errors.

Click here to learn more about Android development with Kotlin