If you’re like me, you’ve grudgingly put up with Java for years, for the sake of getting things done on Android. With Kotlin picking up steam as a viable, modern alternative, things are looking much better for all of us. Whenever possible, I do all my Android work in Kotlin.
To reap the fullest benefits of Kotlin, though, we can’t just fall back on old habits; it’s very easy to write code in a familiar Java style. In fact, allowing this was a design goal of Kotlin, to make it easier to transition between languages. But the real power and beauty of Kotlin is where it differs from Java, sometimes drastically. Let’s take a look at some examples of code patterns I’ve run across in production code, and different ways we can improve upon them.
Imagine you had the following class in Java:
1 2 3 4 5 6 7 8 9 | class Foo { private int mVal = 0; private Bar mBar = null; @Nullable public Bar getBar() { return mBar; } public void setBar(@Nullable Bar bar) { mBar = bar; } } |
Every time we wanted to use mBar
, we’d have to do a check to make sure it’s not null
. In java, if we added a printing function, that might look something like:
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Foo { // ... public void print() { if (mBar != null) { mBar.printName(); } out.println("Foo val: " + mVal); if (mBar != null) { mBar.printSummary(); } } } |
Kotlin takes a very strong stance on nullability, and gives us a number of excellent tools for dealing with nullable state. But it’s not always obvious how best to leverage the power that the language has on offer. If we just write Java code with Kotlin syntax, it can feel like the compiler is fighting with you.
For instance, if we were “super sure” that print()
would never be called before mBar
was initialized, we could simplify the print()
method to remove the null-checks. Kotlin won’t let you get away with that. It says, “No way, you said this reference could be null. You have to convince me it’s not!” Even worse, it considers anything allocated on the heap (i.e. a class member field) vulnerable to having been changed out from under you in another thread, resulting in race conditions. This is probably not actually a concern much of the time, but the Kotlin compiler can’t know this is the case. So the naïve Kotlin version has even more boilerplate:
1 2 3 4 5 6 7 8 9 10 11 12 13 | fun print() { // Capture a local copy on the stack, which we know // can't be modified by another thread. val bar = mBar if (bar != null) { bar.printName() } out.println("Foo val: $mVal") if (bar != null) { bar.printSummary() } } |
But wait, isn’t part of the reason we love Kotlin is that we have to write less boilerplate than in Java? Well, yes. This code is not especially idiomatic Kotlin. We should be using the language’s special syntax, standard library routines, and extension methods to help us deal with null
s in our code. Let’s take a look at the most straightforward Kotlinification of the above method:
1 2 3 4 5 | fun print() { mBar?.printName() out.println("Foo val: $mVal") mBar?.printSummary() } |
The ?.
operator means, “If the thing to the left is not null, do the thing on the right, else return null.” As an engineer who’s read and written some Kotlin, you’ve already seen this. But maybe you don’t think of it immediately, if you’re more comfortable in Java. This is arguably the number one syntactic advantage of Kotlin, so keep it in the front of your mind.
How would we refactor this code if our requirements change, necessitating more complex logic? Say we need to print the entire thing as written above if we have mBar
, and print something else if we don’t? We could wrap the whole thing in an if
block, and put the “something else” in the else
block. This is 100% how I’d write this in Java, and it’s still a valid thing to do in Kotlin. But we have many additional options:
1 2 3 4 5 6 7 | fun print() { mBar?.let { bar -> bar.printName() out.println("Foo val: $mVal") bar.printSummary() } ?: out.println("Foo has no bar (with val: $mVal)") } |
There’s a lot going on here for such a small piece of code. Let’s break it down.
T.let{}
is an extension method that takes a single lambda block as its only argument, and all it does is call the lambda and return its value. Why is this useful? Two reasons: first, in conjunction with the?.
operator, it allows you to conditionally execute the code block you’re passing in; and second, it allows you to bind the receiver (mBar
in this case) to a new parameter which we now know not to benull
, in this case namedbar
.- After the let-block, we’ve got the
?:
operator, the “Elvis operator”. It’s often used in assignments function calls to provide a non-null fallback value. But more generally, it means, “If the expression on the left is null, execute and return the expression on the right.” Used together withlet{}
, it gives us a very concise alternative to an if/else with an auxiliary local reference declaration.
Maybe we notice that most of what we’re doing is calling methods on mBar
. Fortunately for us, let{}
has a sibling that will let us write even less code:
1 2 3 4 5 6 7 | fun print() { mBar?.run { printName() out.println("Foo val: $mVal") printSummary() } ?: out.println("Foo has no bar (with val: $mVal)") } |
T.run{}
is just like let{}
, except instead of passing the object we called it on to the lambda block as an argument, it uses that object as the this
pointer, effectively making the block a temporary extension method.
What if our requirements have changed again, and now we just want to print nothing if mBar
is null
? We could just kill the dangling elvis operator and alternative print call and be done with it. But for stylistic reasons, we may prefer to minimize nested blocks and indentation. Kotlin’s semantics let us write very concise “exit-early” or “return early” code to bail when there’s nothing to do:
1 2 3 4 5 6 7 | fun print() { val bar = mBar ?: return bar.printName() out.println("Foo val: $mVal") bar.printSummary() } |
We’re taking advantage here of the elvis operator in conjunction with the fact that in Kotlin, almost everything, including return
and throw
, is an expression. The compiler can deduce that if mBar
was null
, we’d hit the return
and bail early. Thus, it knows that bar
can never be null
, and we’re free to use it however we want.
Let’s say you’ve got yourself a nullable Foo?
reference now, and you want to call its .print()
method. You’re not 100% sure at this point if you have an actual Foo
instance yet, but you know you haven’t initialized its Bar
field yet. This is a pretty common kinda situation, and exactly how you handle it will depend on your algorithms, app state, and business logic. If we assume we want to initialize the mBar
field and print the resulting object state, in the case where we do have a Foo
instance, the T.apply{}
extension method will help us out here.
1 | foo?.apply { bar = Bar() } ?.print() |
Just like run{}
, apply{}
takes the object it’s called on as a receiver rather than an argument. And just like let{}
and run{}
, using apply{}
with the ?.
operator lets us conditionally execute a block of code. But apply{}
is different from its siblings. Instead of returning the result of its block, apply{}
returns the receiver of the call, i.e. foo
, which is useful for chaining “fluent” calls. Thus, and we can initialize the mBar
field using Kotlin’s property syntax without having to capture the reference, make an assignment, and then call foo.print()
.
If instead your logic requires that if foo
hasn’t been initialized yet, you need to use a default Foo
, you can do something like this:
1 | (foo ?: Foo()).apply { bar = Bar() } .print() |
At the end of this chain, foo
still isn’t set, but you’re able to at least give a sensible response. If the requirement is that foo
must be initialized after we’ve executed this code, we can accommodate that too:
1 | (foo ?: run { foo = Foo(); foo })?.apply { bar = Bar() } ?.print() |
The semicolon finally makes an appearance! Assignment is one of the few things that isn’t an expression in Kotlin, which is why we have to separately offer up the newly assigned reference as the lambda result. This is starting to get a little messy, but it illustrates that Kotlin has numerous and varied facilities for writing code the way that makes the most sense to you, in context. We can clean this up a little by killing that semicolon and reordering the steps:
1 | foo = (foo ?: Foo()).apply { bar = Bar(); print() } |
First we pick either the Foo
we already have, or we allocate a new one. Then we initialize that object’s bar
property (mBar
), and call Foo.print()
. Finally, whichever Foo
we ended up picking will be assigned to our reference, foo
. This is a little bit inefficient in the case that foo
is already initialized, because the same value will be reassigned back to itself. But in practice, this is basically inconsequential.
Lets run down the various facilities we’ve looked at so far:
- The
?.
operator- Lets you write call chains with impunity, secure in the knowledge you won’t throw an NPE
- The
?:
operator- Lets you specify a default value or a fallback code path, if a value is
null
- Lets you specify a default value or a fallback code path, if a value is
T.let(block: (T) -> R): R
- Lets you (conditionally, with
?.
) run a block to use an instance, and return a result
- Lets you (conditionally, with
T.run(block: T.() -> R): R
- Lets you (conditionally) run a block to make calls on an instance, and return a result
T.apply(block: T.() -> Unit): T
- Lets you (conditionally) run a block to make calls on an instance, and returns that instance
There’s many others possible tools in our box that we could have used, like lateinit
variables, or the !!
operator (though we should use that one sparingly). And the recent Kotlin 1.1 release has introduced even more building blocks from which we can compose quite advanced logical flows using very simple and easy to read code.
So next time you’re hacking some Kotlin, and you find yourself thinking, “Is there a better, more Kotliny way to write this?” go ahead and spend the extra 5 or 10 minutes thinking about it; read through the reference and library sources, hit up StackOverflow, or dig through some open source projects for inspiration. Your code will be better for it, and you’ll save time and brain cycles later when you have to maintain the code you wrote.