Kotlin Festive Selection Box

Kotlin Festive Selection Box

Given we are well and truly in the midst of the festive season and I’m thinking about selection boxes I thought it would be a good idea to come up with a Kotlin selection box that goes into a little bit of detail on the language features I have come to like most in my time using the language so far…

Data Classes

Data classes are a concise way to create classes and their associated getter and setter functions with minimal boilerplate. The main purpose of these classes is to hold data, as it says on the tin. There are a few constraints that must be adhered to with data classes; the primary constructor must have at least one parameter, all parameters must have a val or var prefix and they cannot be open, sealed or inner classes. Data classes also provide the copy function to copy an immutable data class while amending one or more of its properties. Finally, data classes provide overridden equals, hashCode and toString implementations that allow for easier comparisons in terms of structural equality of objects.

                        
                            data class Pizza(
	val base: String,
 	val toppings: List<String>,
	val size: Int
)

val order = Pizza(base: "thin", listOf("pepperoni", "peppers"), size: 12)
val updatedOrder = order.copy(base = "stuffed crust")
                        
                    

Delegated Properties

These are properties not defined by a backing field but delegate setting and getting to another piece of code. The main benefit of this is that we can refactor out code that we’d otherwise repeat and share between the various elements that require such code. This helps promote DRY. We use the ‘by’ keyword to indicate the property is controlled by the specified delegate.

                        
                            class IceCream {
	val flavour: String = ""
	val type: String by this::flavour
}
                        
                    

Elvis Operator

The elvis operator is one of several facets of the Kotlin standard library which allows us to handle possible null scenarios more gracefully. It is a binary operator that returns the left-hand side of the expression if that expression yields a result that is not null. In the event where the left-hand side of the expression does yield null it returns the result of the expression on the right-hand side of the elvis operator.

                        
                            data class Pizza(
	val base: String,
	val toppings: List<String>,
	val size: Int? = null
)

val order = Pizza(base: "thin", listOf("pepperoni", "peppers"))
val size = order.size ?: 12
                        
                    

Extension Functions

These functions are perhaps my favourite feature of the language. They are a classic example of the evolution of modern languages making patterns that have stood the test of time somewhat redundant. With extension functions no longer must we rely on the decorator pattern to extend the functionality of an existing class and the incur the overhead of the boilerplate code that comes with it. With extension functions we can extend standard library classes or custom classes in a concise, care-free way.

                        
                            data class Pizza(
	val base: String,
 	val toppings: List<String>,
	val size: Int? = null
)

fun Pizza.baseDisplayName() = base.uppercase()
fun Int.cubed() = this*this*this
                        
                    

Infix Functions

Infix functions are functions that allow us to call them without using the standard period and brackets. In doing this it allows the function to read more like plain English. The ‘by’ function touched on earlier on in this piece for property delegation is an example of a standard library infix function. We can also define our own.

                        
                            data class Pizza(
	val base: String,
	val toppings: List<String>,
	val size: Int? = null,
	val price: Double,
)
  
data class Offer(
	val threshold: Double = 15.00
)
  
infix fun Pizza.qualifiesFor(offer: Offer) = this.price > offer.threshold
val order = Pizza(base: "thin", ListOf("Pepperoni"), size: 12, price 20.00)
val offer = Offer()
val shouldApplyOffer = order qualifiesFor offer
                        
                    

Local Functions

One of the features I’ve not used much in my time writing Kotlin but these can be useful from the point of view of making your code more readable and avoiding long unwieldy functions. If you need a function that doesn’t necessarily belong to a class and you’ll only ever have to use it once, in one place a local function is a good candidate.

                        
                            data class Offer(
	val threshold: Double = 15.00
) {
	fun displayOrderDetails: String {
		fun getOrderPrefix() = "Congratulations you have qualified for our "
		return getOrderPrefix().plus(other: "new offer!")
  }
}
                        
                    

Null Safe Calls

One of the nicest things about Kotlin is how it handles null scenarios far more gracefully than most languages. Not being able to fall foul of null pointer exceptions is a myth but you must be explicit about the places where you can fall into that sort of trap by using the ‘!!’ operator. The null safety operator (?) allows us to avoid unnecessary null checks and the boilerplate that comes with it and makes our code more concise.

                        
                            data class Pizza(
	val base: String,
	val toppings: List<String>,
	val price: Double,
	val size: Int? = null,
	val offer: Offer? = nUll
)

data class Offer(
	val threshold: Double = 15.00
)
  
val order = Pizza(base: "thin", listOf ("Pepperoni"), price: 15.00)

// null safe call that will return the threshold or null
val appliedOffers = order.offer?.threshold

// A null call that is not safe which could result in an NE
val offers = order.offer!!.threshold
                        
                    

Smart Casts

Kotlin provides smart casts that allow us to reduce the boilerplate code required to perform casts. Using the ‘is’ keyword to check the type of an object allows us to treat that object as the implied type in the following block of code if the conditional checking it yields true. In most other languages we would incur an extra step after the check where we would have to then perform the actual cast. This approach to casting is considered safe casting.

                        
                            data class Pizza(
	val base: String,
	val toppings: List<String>,
	val price: Double,
	val size: Int? = null,
	val offer: Offer? = nUll
)

data class Offer(
	val threshold: Double = 15.00
)

val order = Pizza(base: "thin", listOf("Pepperoni"), price: 15.00)
	
fun printOrder() {
	if (order is Pizza)
		println("You have ordered a Pizza!")
	println("I have no idea what you've ordered!")
}
                        
                    

On the contrary we can also use the unsafe cast operator (as) which in the event we try and cast incompatible types we will get an exception. Touching on the theme of null safety again we can take this one step further however and apply the nullable operator to the unsafe cast operator (as?) allowing us to yield a null value when the cast can’t be performed and thus cast more safely.

                        
                            data class Pizza(
	val base: String,
	val toppings: List<String>,
	val price: Double,
	val size: Int? = null,
	val offer: Offer? = null
)
  
data class Offer(
	val threshold: Double = 15.00
)

val order = Pizza(base: "thin", listOf("Pepperoni"), price 15.00)

// The pizza variable in this scenario can be either null or a Pizza type
val pizza = order as? Pizza
                        
                    

Notice also that the ‘is’ and ‘as’ keywords that we can use to cast are standard library infix functions under the hood, another aspect of the language touched on earlier in this blog.

Type Aliases

Type aliases are a nice little feature of the language that again go some way to allowing you to make your code more concise. They allow us to provide an alternate name for existing types. This is useful when you have a long, hard to read type or perhaps some generic type where it’s hard for the reader to infer the true meaning. Type aliases can also be extended to function types.

                        
                            typealias Toppings = List<String>

data class Pizza(
	val base: String,
	val toppings: Toppings,
	val price: Double,
	val size: Int? = null,
	val offer: Offer? = null
)