DEV Community

Cover image for Exploring the when guards feature in Kotlin 2.1
Jin Lee
Jin Lee

Posted on

Exploring the when guards feature in Kotlin 2.1

When I heard about guard in Kotlin 2.1 announcement, I was confused. Why do we even need this? Isn't a simple if/return enough?

fun foo(data: Data) {
    if (!data.valid) {
        return
    }
    // foo main logic
}
Enter fullscreen mode Exit fullscreen mode

Or is it similar to Swift guard?

func foo(data: Data) {
    guard let isValid = data.valid, isValid else {
        return
    }
    // foo main logic
}
Enter fullscreen mode Exit fullscreen mode

I had to re-read the new feature name and it's actually called when guards. I told myself stop just reading the keyword and look deeper into what it offers

What is when guards Feature?

According to Kotlin doc:

Starting from 2.1.0, you can use guard conditions in when expressions or statements with subjects.
Guard conditions allow you to include more than one condition for the branches of a when expression, making complex control flows more explicit and concise as well as flattening the code structure.

This means you can replace nested conditions in a when branch with a flatter, more readable structure. It's a great way to improve code clarity and reduce unnecessary indentation(s). Let's take a look at an example to demonstrate this.

Prerequisite

But first you need to upgrade your Kotlin to the 2.1.0 (or later) and enable the experimental feature by adding the following compiler flag to your build.gradle.kts file:

kotlin {
    compilerOptions {
        freeCompilerArgs.add("-Xwhen-guards")
   }
}
Enter fullscreen mode Exit fullscreen mode

Otherwise you will run into a compiler error asking you to add the compiler flag

The feature "when guards" is experimental and should be enabled explicitly

Silly Example

Imagine you have when statement with a sealed class as a subject and you want to generate different string value depending on the subject's class type:

sealed class Dessert {
    data class CheeseCake(val type: Type): Dessert() {
        enum class Type {
            NEW_YORK,
            JAPANESE,
            NORMAL,
        }
    }
    object Kitkat: Dessert()
    class Oreo(): Dessert()
}

fun foo(dessert: Dessert) {
    when(dessert) {
        is Dessert.CheeseCake -> "This is cheesecake"
        Dessert.Kitkat -> "This is Kitkat"
        is Dessert.Oreo -> "This is Oreo"
    }
}
Enter fullscreen mode Exit fullscreen mode

Straightforward, right? But what if you want more granular logic for CheeseCake, such as displaying a special message for the NEW_YORK type? Before Kotlin 2.1, this would look like:

fun foo(dessert: Dessert) {
    when(dessert) {
        is Dessert.CheeseCake -> {
            if (dessert.type == Type.NEW_YORK) {
                "This is my favorite cheesecake"
            } else {
                "This is cheesecake"
            }
        }
        Dessert.Kitkat -> "This is Kitkat"
        is Dessert.Oreo -> "This is Oreo"
    }
}
Enter fullscreen mode Exit fullscreen mode

With more complex conditions, this structure quickly becomes cluttered.

Simplifying with when guards

This is where the new feature comes in (Btw it's not actual guard keyword but concept). Instead of having this nested block of code, we can rewrite this into:

when (dessert) {
        is Dessert.CheeseCake if dessert.type == Type.NEW_YORK -> "This is my favorite cheesecake"
        Dessert.Kitkat -> "This is Kitkat"
        is Dessert.Oreo -> "This is Oreo"
        is Dessert.CheeseCake -> "This is cheesecake"
    }
Enter fullscreen mode Exit fullscreen mode

This is cool and looks a lot nicer than before but you might want to ask yourself what if we don't use subject for when statement and achieve the same thing? It would look something like this:

    when {
        dessert is Dessert.CheeseCake && dessert.type == Type.NEW_YORK -> "This is my favorite cheesecake"
        dessert is Dessert.CheeseCake -> "This is cheesecake"
        dessert is Dessert.Kitkat -> "This is Kitkat"
        dessert is Dessert.Oreo -> "This is Oreo"
    }
Enter fullscreen mode Exit fullscreen mode

This works well but does have downsides. Not only Repeating dessert is checks makes the code verbose, but using when as an expression requires an else branch:

fun foo(dessert: Dessert): String {
    return when {
        dessert is Dessert.CheeseCake && dessert.type == Type.NEW_YORK -> "This is my favorite cheesecake"
        dessert is Dessert.CheeseCake -> "This is cheesecake"
        dessert is Dessert.Kitkat -> "This is Kitkat"
        dessert is Dessert.Oreo -> "This is Oreo"
        else -> "This is dessert" // Required by the compiler
    }
}
Enter fullscreen mode Exit fullscreen mode

Adding an else branch for unreachable cases feels redundant and clutters the code.

With this new guard feature, the final code would look like this:

fun foo(dessert: Dessert): String {
    return when (dessert) {
        is Dessert.CheeseCake if dessert.type == Type.NEW_YORK -> "This is my favorite cheesecake"
        Dessert.Kitkat -> "This is Kitkat"
        is Dessert.Oreo -> "This is Oreo"
        is Dessert.CheeseCake -> "This is cheesecake"
    }
}
Enter fullscreen mode Exit fullscreen mode

This approach is concise, readable, and avoids unnecessary branching. If you’ve struggled with tools like Detekt complaining about nested blocks, try this feature out!

Further resources

Top comments (2)

Collapse
 
itsyourbuddysteve profile image
Steven • Edited

Nit: I think the Swift code you meant to do this

func foo(data: Data) {
guard let isValid = data.valid, isValid else {
return
}
// foo main logic
}

K thanks!

Your friend,
Steve

Collapse
 
cheesycoderjin profile image
Jin Lee

Thanks for that. Addressed!