Everything else is just a tool to model your domain in a way that’s easier to read and reason about, and to produce much shorter code. Interfaces define module boundaries and can be mocked to enable simpler testing. Through classes, objects, and data structures, you can model your domain entities. Enums make a list of limited options crystal clear. Extensions allow you to add functionality to existing types, without having access to their original source code, giving advantage of better logic structure. With generics, you can write universal code, promoting reuse. And overloaded operators give that extra edge to common operations that benefit from succinct, crisp syntax.
Enums
Custom types that represent a fixed set of discreet values, a.k.a. enumerations.
Union types
Enums are great, but you cannot pass parameters to them. They are predefined options. Sometimes you need a union type to carry a configuration around.
Kotlin doesn’t support combining arbitrary types together in a union ad lib, but it offers sealed hierarchies with polymorphism to achieve the same effect for a finite set of custom types. Sealed classes and interfaces restrict inheritance to the module, so all children are known when the module is compiled. This way when can guarantee exhaustiveness and can vouch for type safety.
Basic example:
sealed interface UIState {
data object Loading: UIState
data class Content(val message: String): UIState
data class Error(val exception: Exception): UIState
}
Robust example with usage:
enum class DietaryRestriction {
WITH_MEAT, VEGETARIAN, VEGAN
}
sealed class MenuOption(val restriction: DietaryRestriction) {
abstract val quantity: Int
data class ClubSandwich(val darkBread: Boolean, override val quantity: Int):
MenuOption(DietaryRestriction.WITH_MEAT)
data class HummusPlate(val flavor: String, override val quantity: Int):
MenuOption(DietaryRestriction.VEGAN)
}
sealed class Dessert(
restriction: DietaryRestriction
): MenuOption(restriction) {
data class FruitSalad(
val strawberries: Boolean = true,
override val quantity: Int
): Dessert(DietaryRestriction.VEGAN)
data class CremeBrulee(
override val quantity: Int
): Dessert(DietaryRestriction.VEGETARIAN)
}
val order = listOf(
MenuOption.ClubSandwich(darkBread = true, quantity = 1),
Dessert.CremeBrulee(quantity = 2)
)
val orderItemCount = order.sumOf { it.quantity }
val orderEncoded = order.map {
when (it) {
is MenuOption.ClubSandwich -> "🥪"
is MenuOption.HummusPlate -> "🧆"
is Dessert.FruitSalad -> "🍓"
is Dessert.CremeBrulee -> "🍮"
}.let { symbol ->
"$symbol ${it.quantity}x"
}
}