Sealed Interface

Latest posts
Get notified of the Latest Sport News Update from Our Blog
Share

A sealed interface restricts which classes or objects can implement it. Only the classes or objects that are defined within the same file (or in the same scope) as the sealed interface can implement it.

The primary use case for sealed interfaces is when you want to define an interface that should be implemented only by a specific set of known classes or objects, typically those that are closely related or within the same module or file.

Syntax

To define a sealed interface, you use the sealed modifier before the interface keyword:

sealed interface MySealedInterface {
    // Interface methods
}

Here’s what sealed interfaces offer

  • Limited Implementations: Just as a sealed class limits its subclasses to a predefined set within the same file or module, a sealed interface would restrict its implementations to a specific set of classes or types.
  • Restriction on Implementations: A sealed interface would restrict which classes can implement it, much like how a sealed class restricts subclassing.

Example1 Of Sealed Interface:

Consider a mobile application that manages user authentication states using sealed interfaces to represent different states of authentication.

First, let’s define a sealed interface AuthenticationState to represent different states of user authentication:

sealed interface AuthenticationState {
    fun message(): String
}

object Unauthenticated : AuthenticationState {
    override fun message(): String = "User is not authenticated."
}

data class Authenticated(val username: String) : AuthenticationState {
    override fun message(): String = "Welcome, $username!"
}

object Authenticating : AuthenticationState {
    override fun message(): String = "Authenticating..."
}

In this example:

  • AuthenticationState is a sealed interface representing different authentication states.
  • Unauthenticated, Authenticated, and Authenticating are classes implementing the AuthenticationState interface, each providing a specific behavior (message) corresponding to its state.

Now, let’s use these interface implementations in a simple authentication scenario:

fun authenticateUser(username: String?, password: String?): AuthenticationState {
    if (username.isNullOrBlank() || password.isNullOrBlank()) {
        return Unauthenticated
    }

    // Simulate authentication process (e.g., validating username and password)
    val isAuthenticated = checkCredentials(username, password)

    return if (isAuthenticated) {
        Authenticated(username)
    } else {
        Unauthenticated
    }
}

fun checkCredentials(username: String, password: String): Boolean {
    // Simulated logic to check if credentials are valid
    return username == "user" && password == "password"
}

fun main() {
    // Example usage to authenticate a user
    val username = "user"
    val password = "password"

    val authenticationState = authenticateUser(username, password)

    // Handle different authentication states
    when (authenticationState) {
        is Authenticated -> println(authenticationState.message())
        is Unauthenticated -> println(authenticationState.message())
        is Authenticating -> println(authenticationState.message())
    }
}

In this example:

  • The authenticateUser function takes username and password as input parameters and returns an AuthenticationState based on the authentication result.
    • If username or password is blank, it returns Unauthenticated.
    • Otherwise, it checks the credentials using checkCredentials function.
      • If the credentials are valid (checkCredentials returns true), it returns Authenticated with the username.
      • Otherwise, it returns Unauthenticated.
  • The main function demonstrates how to use authenticateUser and handle different authentication states using a when expression.
    • It prints the appropriate message based on the authentication state.

Explanation

  • The AuthenticationState sealed interface provides a clear and structured way to represent different states related to user authentication.
  • The implementations (Unauthenticated, Authenticated, Authenticating) encapsulate specific behaviors (message()) associated with each state.
  • The authenticateUser function demonstrates how to use these sealed interface implementations to determine and return the appropriate authentication state based on the input parameters (username and password).
  • The main function shows how to use authenticateUser in a real-world scenario, handle the returned AuthenticationState, and print the corresponding messages based on the state.

Example2 : Managing Loading States in Network Requests

sealed interface NetworkState {
    fun message(): String
}

object Loading : NetworkState {
    override fun message(): String = "Loading..."
}

data class Success(val data: List<String>) : NetworkState {
    override fun message(): String = "Data loaded successfully"
}

data class Error(val errorMessage: String) : NetworkState {
    override fun message(): String = "Error: $errorMessage"
}

// Usage in ViewModel or Repository
fun fetchData(): LiveData<NetworkState> {
    val resultLiveData = MutableLiveData<NetworkState>()
    resultLiveData.value = Loading

    // Simulate network request
    CoroutineScope(Dispatchers.IO).launch {
        delay(2000) // Simulate network delay
        val data = listOf("Item 1", "Item 2", "Item 3")
        resultLiveData.postValue(Success(data))
        // In case of error: resultLiveData.postValue(Error("Network error occurred"))
    }

    return resultLiveData
}

here we define a sealed interface NetworkState and three sealed classes (Loading, Success, Error) that implement it to represent different states of a network request (loading, success with data, error with message). The fetchData() function returns a LiveData<NetworkState> and simulates a network request asynchronously, updating the LiveData with the appropriate state (Loading, Success, or Error) based on the outcome. This pattern allows for clear and type-safe handling of network request states in an Android ViewModel or Repository, enabling UI updates based on the observed state (e.g., showing loading indicator, displaying loaded data, handling error messages).

Benefits

  • Controlled Interfaces: It could be useful in scenarios where an interface is meant to have a fixed number of implementing classes.
  • Enhanced Compile-Time Checks: Similar to sealed classes, sealed interfaces could help in ensuring exhaustive handling of interface implementations at compile time.
  • Improved Type Safety: By limiting the implementations, sealed interfaces enhance type safety and make the code more predictable.

Limitations

  • Visibility Restriction: Sealed interfaces are most effective within the same file or limited scope where the implementing classes are known.
  • Use Case Specific: They work well in scenarios where you want a controlled hierarchy of interfaces.

Also Read :

Related blogs

Nullam quis risus eget urna mollis ornare vel eu leo. Aenean lacinia bibendum nulla sed 

Subscribe to get 15% discount