We are constantly missing some things. Some of them are more important than the others, but it is never too late to catch up with those. Kotlin language brings tons of new concepts and features to your miserable programming life and it is really hard to use all of them in your daily duties. After almost two years of using Kotlin in production, the language itself can give me joy and satisfaction. How is it possible? Because of its many small sweet sugars .
In this article, I would like to share with you my favorite Kotlin candies that I discovered when I was in need of writing robust and concise components for Android applications. To make this article more friendly to read I divided it into three parts. In the first part, you will be able to see some cool features of sealed class and when() control flow function. Enjoy!
Seal your class with a kiss of “pattern matching”
Recently I had a chance to work with Swift. Not only did I have to review the code, I also had to translate some of the components into Kotlin. The more code I read, the more amazed I was, but the most attractive feature for me were enums . Unfortunately, Kotlin enums are not very versatile so I had to find a suitable replacement for them: Sealed classes.
Sealed classes are nothing new in the programming world. Actually, the sealed class is a pretty well-known language concept. Kotlin introduces the sealed keyword that can be added to a class declaration and used to represent restricted class hierarchies, where a value can have one type from a limited set, but cannot have any other type. Long story short, you can use sealed classes as a replacement for enums and much more.
Let’s examine the following lines of code.
sealed class Response data class Success(val body: String): Response() data class Error(val code: Int, val message: String): Response() object Timeout: Response()
At first glance, this code does nothing except declare a poor inheritance relationship, but after step by step deconstruction it shows astonishing potential. So, what actually does the sealed keyword add to the Response class? The best way to unmask it is to use the IntelliJ IDEA Kotlin Bytecodetool.
Step 1. Reveal Kotlin Bytecode
Step 2. Decompile Kotlin Bytecode to Java code
After this super easy transformation, you can start reading the Java representation of your Kotlin code.
public abstract class Response { private Response() { } // $FF: synthetic method public Response(DefaultConstructorMarker $constructor_marker) { this(); } }
As you have probably already guessed, sealed classes are made especially for inheritance, so they are abstract out of the box. But how are they similar to enums? Here is the moment when the Kotlin compiler does you a huge favour by allowing you to use the subclasses of the Response class as cases of the when() function. Additionally, Kotlin provides great flexibility where structures that inherit from a sealed class can be declared as data or even as an object.
fun sugar(response: Response) = when (response) { is Success -> ... is Error -> ... Timeout -> ... }
Not only does it give fully exhaustive expression, it also provides automatic casting so you can use a Response instance without any additional transformations.
fun sugar(response: Response) = when (response) { is Success -> println(response.body) is Error -> println("${response.code} ${response.message}") Timeout -> println(response.javaClass.simpleName) }
Can you imagine how ugly and complicated it could look without a sealedfeature, or without Kotlin at all? If you have forgotten some of the Java language, use IntelliJ IDEA Kotlin Bytecode once again, but sit tight — it might make you faint.
public final void sugar(@NotNull Response response) { Intrinsics.checkParameterIsNotNull(response, "response"); String var3; if (response instanceof Success) { var3 = ((Success)response).getBody(); System.out.println(var3); } else if (response instanceof Error) { var3 = "" + ((Error)response).getCode() + ' ' + ((Error)response).getMessage(); System.out.println(var3); } else { if (!Intrinsics.areEqual(response, Timeout.INSTANCE)) { throw new NoWhenBranchMatchedException(); } var3 = response.getClass().getSimpleName(); System.out.println(var3); } }
Summing up, I was delighted to use the sealed keyword in such cases because it let me shape my Kotlin code in a fashion similar to the Swift version ❤️.
Use the when() function to permute like a boss
As you have already had a chance to see the power of when() in use with the sealed classes, I decided to share more of its robust capability with you. Imagine that you have to implement a function that accepts two enums and produces an immutable state.
enum class Employee { DEV_LEAD, SENIOR_ENGINEER, REGULAR_ENGINEER, JUNIOR_ENGINEER } enum class Contract { PROBATION, PERMANENT, CONTRACTOR, }
enum class Employee describes all roles that can be found in the Company XYZ and enum class Contract contains all types of an employment contract. Based on these two enums you should return a correct SafariBookAccess. Moreover, your function has to produce the state for all permutations of the given enums. As the first step, let’s prototype the signature of the state-producing function.
fun access(employee: Employee, contract: Contract): SafariBookAccess
Now it is time to define theSafariBooksAccess structure and because you are already aware of the sealed keyword, it is a perfect time to use it. It is not necessary to seal SafariBookAccess but it is a good way to encapsulate different state definitions in different cases of SafariBookAccess.
sealed class SafariBookAccess data class Granted(val expirationDate: DateTime) : SafariBookAccess() data class NotGranted(val error: AssertionError) : SafariBookAccess() data class Blocked(val message: String) : SafariBookAccess()
So what is the main idea behind of the access() function? Permutation! Let’s permute .
fun access(employee: Employee, contract: Contract): SafariBookAccess { return when (employee) { SENIOR_ENGINEER -> when (contract) { PROBATION -> NotGranted(AssertionError("Access not allowed on probation contract.")) PERMANENT -> Granted(DateTime()) CONTRACTOR -> Granted(DateTime()) } REGULAR_ENGINEER -> when (contract) { PROBATION -> NotGranted(AssertionError("Access not allowed on probation contract.")) PERMANENT -> Granted(DateTime()) CONTRACTOR -> Blocked("Access blocked for $contract.") } JUNIOR_ENGINEER -> when (contract) { PROBATION -> NotGranted(AssertionError("Access not allowed on probation contract.")) PERMANENT -> Blocked("Access blocked for $contract.") CONTRACTOR -> Blocked("Access blocked for $contract.") } else -> throw AssertionError() } }
This code is faultless, but can you make it more Kotlin-ish? What would you suggest while doing the day-by-day code review of your colleague’s PR/MR? Your comments can contain something like this:
– Too many when() functions. Use Pair to avoid nesting.
– Change the order of enums parameters. Define pair as a Pair<Contract, Employee>() object to make it more readable.
– Merge duplicated return cases.
– Change to single a expression function.
fun access(contract: Contract, employee: Employee) = when (Pair(contract, employee)) { Pair(PROBATION, SENIOR_ENGINEER), Pair(PROBATION, REGULAR_ENGINEER), Pair(PROBATION, JUNIOR_ENGINEER) -> NotGranted(AssertionError("Access not allowed on probation contract.")) Pair(PERMANENT, SENIOR_ENGINEER), Pair(PERMANENT, REGULAR_ENGINEER), Pair(PERMANENT, JUNIOR_ENGINEER), Pair(CONTRACTOR, SENIOR_ENGINEER) -> Granted(DateTime(1)) Pair(CONTRACTOR, REGULAR_ENGINEER), Pair(CONTRACTOR, JUNIOR_ENGINEER) -> Blocked("Access for junior contractors is blocked.") else -> throw AssertionError("Unsupported case of $employee and $contract") }
Now it looks much cleaner and more concise, but Kotlin has some additional sugar that allows you to omit the Pair definition completely . Bam!
fun access(contract: Contract, employee: Employee) = when (contract to employee) { PROBATION to SENIOR_ENGINEER, PROBATION to REGULAR_ENGINEER -> NotGranted(AssertionError("Access not allowed on probation contract.")) PERMANENT to SENIOR_ENGINEER, PERMANENT to REGULAR_ENGINEER, PERMANENT to JUNIOR_ENGINEER, CONTRACTOR to SENIOR_ENGINEER -> Granted(DateTime(1)) CONTRACTOR to REGULAR_ENGINEER, PROBATION to JUNIOR_ENGINEER, CONTRACTOR to JUNIOR_ENGINEER -> Blocked("Access for junior contractors is blocked.") else -> throw AssertionError("Unsupported case of $employee and $contract") }
I hope that you have found this useful, as this construct made my life easier and my Kotlin codebase happier. Unfortunately, it is not possible to use this syntax for triples… isn’t it? Of course this sugar can be applied for triples .
Triple(enum1, enum2, enum3) == enum1 to enum2 to enum3 ❤️
That’s all for the first part, but if you’re still hungry, stay tuned for Part 2. It will be delivered shortly. Cheers!