Gain Clarity on the State Design Pattern in Kotlin: A Step-by-Step Guide

Table of Contents

Have you ever had to manage an object’s behavior based on its state? You might have ended up writing a series of if-else or when statements to handle different scenarios. Sound familiar? (Especially if you’re working with Android and Kotlin!) If so, it’s time to explore the State Design Pattern—a structured approach to simplify your code, enhance modularity, and improve maintainability.

In this blog, we’ll explore the State Design Pattern in depth, focusing on its use in Kotlin. We’ll discuss its purpose, the benefits it offers, and provide detailed examples with clear explanations. By the end, you’ll have the knowledge and confidence to incorporate it seamlessly into your projects.

State Design Pattern

The State Design Pattern is part of the behavioral design patterns group, focusing on managing an object’s dynamic behavior based on its current state. As described in the Gang of Four’s book, this pattern “enables an object to modify its behavior as its internal state changes, giving the impression that its class has changed.” In short, it allows an object to alter its behavior depending on its internal state.

Key Features of the State Pattern

  • State Encapsulation: Each state is encapsulated in its own class.
  • Behavioral Changes: Behavior changes dynamically as the object’s state changes.
  • No Conditionals: It eliminates long if-else or when chains by using polymorphism.

Structure of the State Design Pattern

State pattern encapsulates state-specific behavior into separate classes and delegates state transitions to these objects. Here’s a breakdown of its structure:

State Interface

The State Interface defines the methods that each state will implement. It provides a common contract for all concrete states.

Kotlin
interface State {
    fun handle(context: Context)
}

Here,

  • The State interface declares a single method, handle(context: Context), which the Context calls to delegate behavior.
  • Each concrete state will define its behavior within this method.

Concrete States

The Concrete States implement the State interface. Each represents a specific state and its associated behavior.

Kotlin
class ConcreteStateA : State {
    override fun handle(context: Context) {
        println("State A: Handling request and transitioning to State B")
        context.setState(ConcreteStateB()) // Transition to State B
    }
}

class ConcreteStateB : State {
    override fun handle(context: Context) {
        println("State B: Handling request and transitioning to State A")
        context.setState(ConcreteStateA()) // Transition to State A
    }
}
  • ConcreteStateA and ConcreteStateB implement the State interface and define their unique behavior.
  • Each state determines the next state and triggers a transition using the context.setState() method.

Context

The Context is the class that maintains a reference to the current state and delegates behavior to it.

Kotlin
class Context {
    private var currentState: State? = null

    fun setState(state: State) {
        currentState = state
        println("Context: State changed to ${state::class.simpleName}")
    }

    fun request() {
        currentState?.handle(this) ?: println("Context: No state is set")
    }
}
  • The Context class holds a reference to the current state via currentState.
  • The setState() method updates the current state and logs the transition.
  • The request() method delegates the action to the current state’s handle() method.

Test the Implementation

Finally, we can create a main function to test the transitions between states.

Kotlin
fun main() {
    val context = Context()

    // Set initial state
    context.setState(ConcreteStateA())

    // Trigger behavior and transition between states
    context.request() // State A handles and transitions to State B
    context.request() // State B handles and transitions to State A
    context.request() // State A handles and transitions to State B
}

Output

Kotlin
Context: State changed to ConcreteStateA
State A: Handling request and transitioning to State B
Context: State changed to ConcreteStateB
State B: Handling request and transitioning to State A
Context: State changed to ConcreteStateA
State A: Handling request and transitioning to State B
Context: State changed to ConcreteStateB

How These Components Work Together

  1. The Context is the central point of interaction for the client code. It contains a reference to the current state.
  2. The State Interface ensures that all states adhere to a consistent set of behaviors.
  3. The Concrete States implement specific behavior for the Context and may trigger transitions to other states.
  4. When a client invokes a method on the Context, the Context delegates the behavior to the current state, which executes the appropriate logic.

Real-Time Use Cases

Game Development

Gun Fire Squade Battleground

The State Design Pattern is widely used in game development. A game character can exist in various states, such as healthy, surviving, or dead. In the healthy state, the character can attack enemies using different weapons. When the character enters the surviving state, its health becomes critical. Once the health reaches zero, the character transitions into the dead state, signaling the end of the game.

Now, let’s first explore how we could implement this use case without using the State Design Pattern, and then compare it with an implementation using the State Pattern for better understanding. This can be done using a series of if-else conditional checks, as demonstrated in the following code snippets.

Player Class

Kotlin
class Player {
    fun attack() {
        println("Attack")
    }

    fun fireBomb() {
        println("Fire Bomb")
    }

    fun fireGunblade() {
        println("Fire Gunblade")
    }

    fun fireLaserPistol() {
        println("Laser Pistol")
    }

    fun firePistol() {
        println("Fire Pistol")
    }

    fun survive() {
        println("Surviving!")
    }

    fun dead() {
        println("Dead! Game Over")
    }
}

Now let us define our game context class which defines the different actions conditionally depends on the state of the player.

GameContext Class (Without State Pattern)

Kotlin
class GameContext {
    private val player = Player()

    fun gameAction(state: String) {
        when (state) {
            "healthy" -> {
                player.attack()
                player.fireBomb()
                player.fireGunblade()
                player.fireLaserPistol()
            }
            "survival" -> {
                player.survive()
                player.firePistol()
            }
            "dead" -> {
                player.dead()
            }
            else -> {
                println("Invalid state")
            }
        }
    }
}

In this implementation of GameContext, we’re using a when block (or even if-else statements) to handle the state. While this approach works well for smaller examples, it can become harder to maintain and less scalable as more states and behaviors are added.

Applying the State Design Pattern

To eliminate the need for multiple conditional checks, let’s refactor the code using the State Design Pattern. We will define separate state classes for each state, and the GameContext will delegate the actions to the appropriate state object.

Define the State Interface

Kotlin
interface PlayerState {
    fun performActions(player: Player)
}

Implement Concrete States

Now, we’ll create concrete state classes for each state: HealthyState, SurvivalState, and DeadState.

Kotlin
class HealthyState : PlayerState {
    override fun performActions(player: Player) {
        player.attack()
        player.fireBomb()
        player.fireGunblade()
        player.fireLaserPistol()
    }
}

class SurvivalState : PlayerState {
    override fun performActions(player: Player) {
        player.survive()
        player.firePistol()
    }
}

class DeadState : PlayerState {
    override fun performActions(player: Player) {
        player.dead()
    }
}

Modify the GameContext Class

Finally, the GameContext class will hold a reference to the current state and delegate the action calls to that state.

Kotlin
class GameContext {
    private val player = Player()
    private var state: PlayerState = HealthyState() // Default state

    fun setState(state: PlayerState) {
        this.state = state
    }

    fun gameAction() {
        state.performActions(player)
    }
}

Testing the State Design Pattern

Now, let’s demonstrate how we can switch between different states and let the player perform actions based on the current state:

Kotlin
fun main() {
    val gameContext = GameContext()

    println("Player in Healthy state:")
    gameContext.gameAction() // Perform actions in Healthy state

    println("\nPlayer in Survival state:")
    gameContext.setState(SurvivalState())
    gameContext.gameAction() // Perform actions in Survival state

    println("\nPlayer in Dead state:")
    gameContext.setState(DeadState())
    gameContext.gameAction() // Perform actions in Dead state
}

Output

Kotlin
Player in Healthy state:
Attack
Fire Bomb
Fire Gunblade
Laser Pistol

Player in Survival state:
Surviving!
Fire Pistol

Player in Dead state:
Dead! Game Over

Let’s see what benefits we get by using the State Pattern in this case.

  • Cleaner Code: The GameContext class no longer contains any conditionals. The logic is moved to the state classes, making it easier to manage and extend.
  • Modular: Each state is encapsulated in its own class, which improves maintainability. If you need to add new states, you just need to implement a new PlayerState class.
  • Extensible: New actions or states can be added without modifying the existing code. You simply create new state classes for additional behaviors.

Before generalizing the benefits of the State Pattern, let’s look at one more use case, which is in a document editor.

A Document Workflow

We’ll create a simple document editor. A document can be in one of three states:

  1. Draft
  2. Moderation
  3. Published

The actions allowed will depend on the state:

  • In Draft, you can edit or submit for review.
  • In Moderation, you can approve or reject the document.
  • In Published, no changes are allowed.

Without further delay, let’s implement it.

Define a State Interface

The state interface defines the contract for all possible states. Each state will implement this interface.

Kotlin
// State.kt
interface State {
    fun edit(document: Document)
    fun submitForReview(document: Document)
    fun publish(document: Document)
}

Create Concrete State Classes

Each state class represents a specific state and provides its own implementation of the state behavior.

Draft State

Kotlin
// DraftState.kt
class DraftState : State {
    override fun edit(document: Document) {
        println("Editing the document...")
    }

    override fun submitForReview(document: Document) {
        println("Submitting the document for review...")
        document.changeState(ModerationState()) // Transition to Moderation
    }

    override fun publish(document: Document) {
        println("Cannot publish a document in draft state.")
    }
}

Moderation State

Kotlin
// ModerationState.kt
class ModerationState : State {
    override fun edit(document: Document) {
        println("Cannot edit a document under moderation.")
    }

    override fun submitForReview(document: Document) {
        println("Document is already under review.")
    }

    override fun publish(document: Document) {
        println("Publishing the document...")
        document.changeState(PublishedState()) // Transition to Published
    }
}

Published State

Kotlin
// PublishedState.kt
class PublishedState : State {
    override fun edit(document: Document) {
        println("Cannot edit a published document.")
    }

    override fun submitForReview(document: Document) {
        println("Cannot submit a published document for review.")
    }

    override fun publish(document: Document) {
        println("Document is already published.")
    }
}

Define the Context Class

The context class represents the object whose behavior changes with its state. It maintains a reference to the current state.

Kotlin
// Document.kt
class Document {
    private var state: State = DraftState() // Initial state

    fun changeState(newState: State) {
        state = newState
        println("Document state changed to: ${state.javaClass.simpleName}")
    }

    fun edit() = state.edit(this)
    fun submitForReview() = state.submitForReview(this)
    fun publish() = state.publish(this)
}

Test the Implementation

Finally, we can create a main function to test how our document transitions between states.

Kotlin
// Main.kt
fun main() {
    val document = Document()

    // Current state: Draft
    document.edit()
    document.submitForReview()

    // Current state: Moderation
    document.edit()
    document.publish()

    // Current state: Published
    document.submitForReview()
    document.edit()
}

Output

Kotlin
Editing the document...
Submitting the document for review...
Document state changed to: ModerationState
Cannot edit a document under moderation.
Publishing the document...
Document state changed to: PublishedState
Cannot submit a published document for review.
Cannot edit a published document.

Benefits of Using the State Design Pattern

  • Cleaner Code: Behavior is encapsulated in state-specific classes, eliminating conditionals.
  • Scalability: Adding new states is straightforward—just implement the State interface.
  • Encapsulation: Each state manages its behavior, reducing the responsibility of the context class.
  • Dynamic Behavior: The object’s behavior changes at runtime by switching states.

When Should You Use the State Pattern?

The State Pattern is ideal when:

  • An object’s behavior depends on its state.
  • You have complex conditional logic based on state.
  • States frequently change, and new states may be added.

However, avoid using it if:

  • Your application has only a few states with minimal behavior.
  • The state transitions are rare or do not justify the overhead of additional classes.

Conclusion

The State Design Pattern is an excellent way to achieve cleaner, more maintainable, and modular code. By isolating state-specific behaviors within their own classes, we can simplify the logic in our context objects and make our programs easier to extend.

In this blog, we examined the State Pattern through the lens of a document workflow and game development example. I hope this walkthrough provided clarity on implementing it in Kotlin. Now it’s your chance—experiment with this pattern in your own projects and experience its impact firsthand..!

Skill Up: Software & AI Updates!

Receive our latest insights and updates directly to your inbox

Related Posts

error: Content is protected !!