Supercharge Your Code: Harnessing the Power of When Expressions in Kotlin for Enhanced Development
Kotlin is a modern, concise and powerful programming language that has gained a lot of popularity among developers in recent years. One of the features that makes Kotlin stand out is its powerful when expression, which is a more expressive version of the traditional switch statement in Java. In this article, we will dive deep into the power of when expressions in Kotlin.
Syntax of When Expressions in Kotlin
The syntax of the when expression is straightforward and intuitive. It consists of a keyword “when”, followed by a variable or expression in parentheses, and then a series of cases separated by commas. Each case is defined by a value or a range of values, followed by the arrow operator (->) and the block of code to execute. Finally, there is an optional else block, which executes when none of the cases match.
when (variable) {
case1Value -> {
// Code to execute when case1Value matches the variable
}
case2Value, case3Value -> {
// Code to execute when case2Value or case3Value matches the variable
}
in case4Value..case6Value -> {
// Code to execute when the variable is in the range from case4Value to case6Value
}
else -> {
// Code to execute when none of the above cases match
}
}
Examples
Here are some simple examples that demonstrate the power of when expressions in Kotlin:
Matching on Types:
When expressions in kotlin can be used to match on types of objects. For example, the following code matches on a variable x
and executes different blocks of code depending on whether x
is an Int
, a String
, or a Boolean
:
when (x) {
is Int -> {
// Code to execute when x is an integer
}
is String -> {
// Code to execute when x is a string
}
is Boolean -> {
// Code to execute when x is a boolean
}
else -> {
// Code to execute when x is none of the above types
}
}
Matching on Values:
When expressions in kotlin can also be used to match on specific values. For example, the following code matches on a variable x
and executes different blocks of code depending on whether x
is 1, 2, 3 or none of the above:
when (x) {
1 -> {
// Code to execute when x is 1
}
2 -> {
// Code to execute when x is 2
}
3 -> {
// Code to execute when x is 3
}
else -> {
// Code to execute when x is none of the above values
}
}
Matching on Ranges:
When expressions in kotlin can also match on ranges of values. For example, the following code matches on a variable x
and executes a block of code if x
is in the range from 1 to 10:
when (x) {
in 1..10 -> {
// Code to execute when x is in the range from 1 to 10
}
else -> {
// Code to execute when x is not in the range from 1 to 10
}
}
Multiple Conditions:
When expressions in kotlin can match on multiple conditions. For example, the following code matches on a variable x
and executes a block of code if x
is greater than 10 and less than 20:
when (x) {
in 11..19 -> {
// Code to execute when x is in the range from 11 to 19
}
else -> {
// Code to execute when x is not in the range from 11 to 19
}
}
Smart Casting:
When expressions in kotlin can be used to cast an object to a specific type. For example, the following code matches on a variable x
and casts it to an Int
if possible, then executes a block of code that uses the casted Int
:
when (x) {
is String -> {
val length = x.length
// Code to execute using the length of the string
}
is Int -> {
val value = x
// Code to execute using the integer value of x
}
else -> {
// Code to execute when x is neither a String nor an Int
}
}
Returning Values:
When expressions in kotlin can return values, which can be useful in functional programming. For example, the following code matches on a variable x
and returns a corresponding value depending on whether x
is 1, 2, or none of the above:
val result = when (x) {
1 -> "One"
2 -> "Two"
else -> "Other"
}
It’s worth noting that when expressions in kotlin are often used in conjunction with other Kotlin features, such as enum, extension functions, lambdas, and sealed classes, to provide even more powerful and expressive code.
Now let’s see some cool ways to deal with other Kotlin features
Using “when” to deal with enum classes:
Kotlin’s “enum” class is a powerful and flexible way to represent a fixed set of values. When expressions in kotlin can be particularly useful for dealing with enum classes, as they can handle each value in a concise and readable way. For example:
enum class Color {
RED, ORANGE, YELLOW, GREEN, BLUE, INDIGO, VIOLET
}
fun getColorName(color: Color) = when (color) {
Color.RED -> "Red"
Color.ORANGE -> "Orange"
Color.YELLOW -> "Yellow"
Color.GREEN -> "Green"
Color.BLUE -> "Blue"
Color.INDIGO -> "Indigo"
Color.VIOLET -> "Violet"
}
Here, the “when” expression takes in a value of type “Color” and matches each possible value of the enum with a corresponding string. This makes the code more concise and easier to read than a series of “if-else” statements.
Using “when” with arbitrary objects:
In addition to using “when” expressions with enum classes, Kotlin also allows developers to use “when” with arbitrary objects. This can be useful for handling multiple types of objects in a concise and readable way. For example:
fun describeObject(obj: Any): String = when (obj) {
is String -> "String with length ${obj.length}"
is Int -> "Integer with value $obj"
is Long -> "Long integer with value $obj"
else -> "Unknown object"
}
Here, the “when” expression takes in an arbitrary object of type “Any” and matches it against each possible type using “is” checks. This allows the function to provide a concise and readable description of any object that is passed in.
Using “When” with extension functions:
Kotlin also allows developers to use “when” with extention functions in Kotlin, Here’s an example:
fun Int.isEven(): Boolean = this % 2 == 0
fun Int.isOdd(): Boolean = this % 2 == 1
fun Int.classify(): String {
return when {
this.isEven() -> "Even"
this.isOdd() -> "Odd"
else -> "Unknown"
}
}
fun main() {
println(2.classify()) // Output: Even
println(3.classify()) // Output: Odd
println(4.classify()) // Output: Even
println(5.classify()) // Output: Odd
}
In this example, we define two extension functions called isEven
and isOdd
that can be called on integer values. We then define another extension function called classify
that uses a “when” expression to classify integer values as “Even”, “Odd”, or “Unknown”. The “when” expression is called on the integer value using the this
keyword.
Using “When” with Sealed Classes
Using “when” expressions with sealed classes is a powerful feature in Kotlin that allows for concise and type-safe pattern matching.
sealed class AppState {
object Loading : AppState()
data class Success(val data: List<Item>) : AppState()
data class Error(val message: String) : AppState()
}
fun renderState(state: AppState) {
when (state) {
is AppState.Loading -> showLoadingScreen()
is AppState.Success -> showItems(state.data)
is AppState.Error -> showErrorScreen(state.message)
}
}
// Usage:
val state = AppState.Loading
renderState(state)
val data = listOf(Item("Item 1"), Item("Item 2"))
val state = AppState.Success(data)
renderState(state)
val error = "An error occurred"
val state = AppState.Error(error)
renderState(state)
In this example, we define a sealed class called AppState
, which represents the state of an app screen. It has three possible subclasses: Loading
, Success
, and Error
. The Success
subclass contains a list of Item
objects, while the Error
subclass contains an error message.
We then define a function called renderState
that takes an AppState
instance as a parameter and uses a “when” expression to pattern match on the possible subclasses. Depending on the subclass, it calls different functions to render the appropriate screen.
Using “when” expressions with sealed classes in this way allows for more readable and maintainable code, as the pattern matching is type-safe and localized to a single function. It also allows for easy extensibility of the AppState
hierarchy, as new subclasses can be added to represent different states of the app screen.
Using “When” with Lambda
Here’s a simple example of using a “when” expression with a lambda in Kotlin to filter a list of numbers:
val numbers = listOf(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)
val evenNumbers = numbers.filter { number ->
when (number % 2) {
0 -> true // The number is even, so include it in the filtered list
else -> false // The number is odd, so exclude it from the filtered list
}
}
println(evenNumbers) // Prints: [2, 4, 6, 8, 10]
In this example, we start with a list of numbers and use the “filter” function to create a new list that only includes the even numbers. We pass a lambda expression to the “filter” function, which takes each number in the list and applies the “when” expression to determine if it should be included in the filtered list.
The “when” expression checks if the number is divisible by 2 (i.e., if it’s even) and returns true if it is, and false if it’s not. This lambda expression is concise and easy to read, and demonstrates how “when” expressions can be used in practical scenarios to create more expressive and flexible code.
Pattern Matching: To Regex or Custom Type
Here’s an example of how “when” expressions can be used for pattern matching:
fun checkType(obj: Any) {
when(obj) {
is Int -> println("Integer: $obj")
is String -> println("String: $obj")
is List<*> -> println("List: $obj")
else -> println("Unknown Type")
}
}
fun main() {
checkType(1)
checkType("hello")
checkType(listOf(1, 2, 3))
checkType(1.0)
}
In this example, we define a function called checkType
that takes an arbitrary object as its input and uses a “when” expression to perform pattern matching on the object. The expression checks whether the object is an integer, a string, or a list of any type of element, and then prints a message indicating the type of the object.
Note that “when” expressions are not limited to simple type checks, but can also be used for more complex pattern-matching scenarios involving regular expressions or custom types. However, this example demonstrates the basic pattern-matching functionality of “when” expressions.
Lets see couple of examples of how “when” expressions can be used for more complex pattern matching scenarios in Kotlin.
1. Matching against regular expressions:
val input = "Hello123"
val pattern = "d+".toRegex()
val result = when (input) {
pattern.matches(input) -> "Input matches pattern"
else -> "Input does not match pattern"
}
println(result)
In this example, we are using a “when” expression to match the input
string against the regular expression pattern \\d+
, which matches one or more digits. If the input matches the pattern, we print “Input matches pattern”. If it does not match the pattern, we print “Input does not match pattern”.
2. Matching against custom types:
sealed class Animal {
object Dog : Animal()
object Cat : Animal()
object Fish : Animal()
}
fun describeAnimal(animal: Animal) = when (animal) {
is Animal.Dog -> "This is a dog."
is Animal.Cat -> "This is a cat."
is Animal.Fish -> "This is a fish."
}
val myPet = Animal.Dog
println(describeAnimal(myPet))
In this example, we have a sealed class Animal
with three objects: Dog
, Cat
, and Fish
. We then define a function describeAnimal
that takes in an Animal
object and uses a “when” expression to match against the three possible cases of the sealed class. Depending on which case matches, the function returns a description of the animal.
Finally, we create an instance of Animal.Dog
and pass it to describeAnimal
, which prints “This is a dog.” to the console.
Smart casts: combining type checks and casts
You might have an idea about smart casts, as we saw in the previous smart casting example. In Kotlin, “smart casts” are a feature that allows developers to combine type checks and casts in a single operation. This can make code more concise and less error-prone. For example:
fun processString(str: Any) {
if (str is String) {
println(str.toUpperCase())
}
}
Here, the “is” check ensures that the variable “str” is of type “String”, and the subsequent call to “toUpperCase()” can be made without casting “str” explicitly.
This same functionality can be achieved using a “when” expression, like so:
fun processString(str: Any) = when (str) {
is String -> str.toUpperCase()
else -> ""
}
Here, the “when” expression combines the “is” check and the call to “toUpperCase()” in a single operation. If the variable “str” is not a string, the expression returns an empty string.
Refactoring: replacing “if” with “when”:
Kotlin’s “when” expression can be a useful tool for refactoring existing “if-else” code. In many cases, “if-else” statements can be replaced with a “when” expression, which can make the code more concise and easier to read. For example:
fun getMessage(isValid: Boolean): String {
return if (isValid) {
"Valid"
} else {
"Invalid"
}
}
This can be refactored into a “when” expression like so:
fun getMessage(isValid: Boolean): String = when (isValid) {
true -> "Valid"
false -> "Invalid"
}
Here, the “when” expression replaces the “if-else” statement, resulting in more concise and readable code.
Blocks as branches of “if” and “when”:
Kotlin’s “if” and “when” expressions can also be used to evaluate blocks of code, rather than just simple expressions. This can be useful for handling complex logic or multiple statements. For example:
fun evaluateGrade(score: Int): String = when {
score < 60 -> {
"Failing grade"
}
score < 70 -> {
"D"
}
score < 80 -> {
"C"
}
score < 90 -> {
"B"
}
else -> {
"A"
}
}
Here, each branch of the “when” expression contains a block of code, allowing for more complex logic to be evaluated. This can make the code more readable and easier to maintain.
Real-world examples in Android Code
Let’s see some real-world examples of where when expressions in kotlin can be used in Android app code:
Handling different button clicks: In an app with multiple buttons, “when” expressions can be used to handle the click events for each button. For example:
button.setOnClickListener { view ->
when (view.id) {
R.id.button1 -> {
// Handle button1 click event
}
R.id.button2 -> {
// Handle button2 click event
}
// Handle other button click events
}
}
Displaying different views: In a complex app with many different screens, “when” expressions can be used to display the appropriate view for each screen. For example:
when (screen) {
Screen.Home -> {
// Display the home screen view
}
Screen.Profile -> {
// Display the profile screen view
}
// Handle other screen views
}
Handling different data types: In an app that deals with different data types, “when” expressions can be used to handle each data type appropriately. For example:
when (data) {
is String -> {
// Handle string data type
}
is Int -> {
// Handle integer data type
}
// Handle other data types
}
Handling different API responses: In an app that communicates with an API, “when” expressions can be used to handle the different responses from the API. For example:
when (response) {
is Success -> {
// Handle successful API response
}
is Error -> {
// Handle API error response
}
// Handle other API responses
}
When expression in Extention Function: Here’s an example of using “when” expressions with extension functions in an Android app:
fun TextView.setTextColorByStatus(status: String) {
val colorRes = when (status) {
"OK" -> R.color.green
"WARNING" -> R.color.yellow
"ERROR" -> R.color.red
else -> R.color.black
}
this.setTextColor(ContextCompat.getColor(context, colorRes))
}
// usage:
textView.setTextColorByStatus("OK")
When expression in Lambda expression: Lambda expressions are often used in conjunction with “when” expressions in Android. Here’s an example:
val sharedPreferences = getSharedPreferences("MyPrefs", Context.MODE_PRIVATE)
val isDarkModeEnabled = sharedPreferences.getBoolean("isDarkModeEnabled", false)
val statusBarColor = when {
isDarkModeEnabled -> Color.BLACK
else -> Color.WHITE
}
window.statusBarColor = statusBarColor
Let’s see some benefits and limitations of “when” expressions in Kotlin:
Benefits:
- Concise and expressive: “when” expressions can help make your code more concise and expressive, which can improve readability and maintainability.
- More flexible than switch statements: “when” expressions are more flexible than traditional switch statements, as they can handle a wider range of conditions and types.
- Smart casting: “when” expressions can be used to perform smart casting, which can help eliminate the need for explicit type casting in your code.
- Works with enums and arbitrary objects: “when” expressions can be used with enums and arbitrary objects, which can help simplify your code.
- Supports functional programming: “when” expressions can be used with lambda functions, which makes them well-suited to functional programming approaches.
Limitations:
- Limited readability in complex scenarios: “when” expressions can become difficult to read and understand in more complex scenarios, especially when there are nested conditions or multiple actions associated with each condition.
- No fall-through behavior: Unlike switch statements, “when” expressions do not support fall-through behavior. This means that you cannot execute multiple actions for a single condition without using additional code.
- Limited to simple pattern matching: “when” expressions are limited to simple pattern matching, and cannot be used for more complex operations such as complex regular expressions.
- Limited backward compatibility: “when” expressions were introduced in Kotlin 1.1 and are not available in earlier versions of the language.