An opinionated guide on how to make your Kotlin code fun to read and joy to work with

Gabor Varadi
ProAndroidDev
Published in
9 min readFeb 15, 2021

--

I’ve been meaning to write this article in a while.

Hopefully, the following tips and style recommendations (in no particular order) will help you write better Kotlin!

Let’s get on with it, shall we?

For 2 or more constructor arguments, prefer not to keep the properties on the same line as the class name in the constructor definition

class MyClass(
val a: A,
val b: B,

): MyParentClass(a, b), MyInterface {
// ...
}

This format is preferable, because when adding the 3rd argument, important bits (such as a possible base class or interface) can appear too far on the right side of the screen.

Listing them one after the other (including the trailing comma since 1.4.21+) allows for better readability and extensibility (reducing the chance of causing merge conflicts with future code changes, for example).

Prefer to use the scoping function ‘let’ only in assignments or return statements, but NOT as general control flow, or a “quick rename to ‘it’"

Oftentimes, Kotlin code tends to do something like this:

fun myMethod(nullableA: A?) {
nullableA?.let { // no
it.x() // no
it.y() // no
it.z() // no
}
}

(Note: in a method like this, A? could be replaced with A to take advantage of typed nullability, this is just an example.)

To allow better maintainability and readability, we should instead prefer to only use ?.let { in simple assignments, or return statements, but not as general control flow.

For example, the following chain is easy to understand:

val protoClass = savedInstanceState?.getByteArray("protoValue")
?.let { ProtoClass.parseFrom(it) }

However, in this case, ?.let { is part of an assignment.

Also, while ?.let { can be chained with a ?: to provide a default value (especially in combination with takeIf) it shouldn’t be used to execute side-effects (also is more suitable for that).

In fact, it needs to be said: x?.let {} ?: run {} is NOT a general purpose replacement for if-else statements and null-checks, and should never be used in this particular format for that purpose! They are not equivalent and can cause unexpected side-effects.

Avoid all usage of the implicit argument name `it` inside multi-line expressions or multi-line lambdas

Simply put, the name it is for single-line expressions, and simple cases only.

If you see an it in a multi-line function, prefer to rename it to something meaningful (even e for an exception/error has more semantic meaning than it!).

Always specify a name instead of the implicit argument name it inside map, flatMap, switchMap, flatMapLatest and so on lambda argument, and in any function that follows it

If you see a function that maps the current type you’re working with to another type or semantic thing, then the input of the following function should be renamed to something meaningful instead of it .

val namesStartingWithZ = itemList.map { it.name }
.filter { name -> name.startsWith("Z") }

Prefer early-returns combined with the elvis-operator in place of null-checks if there is no else branch

It is fairly common that a field-level mutable nullable field, or for example an observe { block provides optional values, in which case we need to do a null-check.

In this case, people often use ?.let { as a general control flow statement.

While it would be already preferable to assign the variable to a val x = x and do an if (x != null) { check (to use safe-casting while keeping the original variable name):

val x = x // okif (x != null) { // ok
// you can use `x` as a non-null value
}

We can also rely on elvis + return (or sometimes, elvis + break or maybe even elvis + continue):

liveData.observe(viewLifecycleOwner) {
val value = it ?: return@observe
// ...
}

Note: here, the it is only used on a single line to assign it to a semantic name, therefore the argument wasn’t named explicitly so that the null-check could be performed instead.

Always use named arguments for lambda arguments, if 2 or more lambdas are being passed to a function

With standard RxJava, one might see code like this:

observable.subscribe({ // no
showData(it)
}, {
it.printStackTrace() // no
})

However, it is significantly easier to read a function like this, if named arguments are used — as there are 2 or more lambdas being passed to the function.

observable.subscribeBy(
onNext = { data ->
showData(data)
},
onError = { e ->
e.printStackTrace()
}
)

Specify the return type of a function, if the function is public (and isn’t coming from `override` of an interface function)

While it might be tempting to rely on Kotlin’s single-line assignment syntax in the case of simple methods, it’s still better to ensure we are returning the correct, expected type — especially if we are writing a library, but it’s good practice either way.

override fun getItemCount() = 0 // okval date: SimpleDateFormat // typed
get() = SimpleDateFormat("yyyy-MM-dd")

Do not use positional decomposition over data classes that are not intended to be used as tuples

Tuples (like Pair and Triple) contain positional information, and typically do not contain semantic information (an exception is colors — which are technically a tuple of ARGB ordered values, but the arity is not likely to change).

Regular data classes like data class Book should never be used in a positional decomposition like val (title, author) = book (where both properties are String) as it is likely to change, and changes in field order would break existing code in a hard-to-notice way.

In assignments, consider the usage of `when` instead of `if-else`, and always prefer `when` over `} else if {`

While it is fairly common to see code like so:

val x = if (condition) {
0
} else { // eh
1
}

You can make it significantly nicer to read and easier to understand by grouping the conditions and assigned values neatly, using the when { keyword.

val x = when {
condition -> 0
else ->
1
}

Note that when you use this style, the IDE lint might ask you to use when(condition) {, but that makes it unnecessarily more verbose — so if the condition is in fact a mere boolean expression, that shouldn’t be passed to the when(..) itself.

Note that the when(value) { however is extremely useful for enums and sealed classes, in which case the expression (if not in an assignment) should be made exhaustive.

when (enum) {
A -> ...
B -> ...
C -> ...
}.safe()
fun <T> T.safe() = this

Note that when(val x = expression) { can sometimes be used, but it’s fairly situational.

Always prefer `val` over `var` (unless you actually need a `var`)

Goes (almost) without saying: if a value/reference should not be mutable, or can be made immutable, then it should be made immutable.

Avoid exposing mutable data structures as generic arguments or function input parameters

If you see a function that takes ArrayList<T>, or you see a LiveData that contains mutable values like ArrayList, MutableList, LinkedList etc. then that should be just List<T> to take advantage of Kotlin’s immutable collections.

Even in Java, the following pattern is fairly common:

public List<String> someMethod(List<String> values) {
List<String> newValues = new ArrayList<>(values);
// do things on newValues return Collections.unmodifiableList(newValues);
}

But in Kotlin, we can simplify this as the type system disallows modifications to the list (as long as it’s not seen through an interface that exposes mutator methods):

fun List<String>.someMethod(): List<String> {
val newValues = toMutableList()
// do things on newValues return newValues.toList()
}

Note to remember: if you see a LiveData<ArrayList<T>>, it should be LiveData<List<T>>.

Try to avoid _prefix in all variable names if possible, including in backing properties

This is most likely a personal preference, but the private fields of a backing property tend to represent the important things, thus a _ prefix makes it read as noisy.

If you can give it a better name than using _name, consider using that name instead, like currentName.

Only use internal visibility if that’s actually what you need

Many single module apps use internal as a replacement for Java’s package visibility, even though in a single module app, internal has no effect. Either private or public visibility makes the intent clearer (in a non-library module) than internal does.

Try to avoid creating “__Util” classes/objects if you can use a top-level extension function

“Static helper functions” can now be made top-level, and oftentimes, it can create more idiomatic Kotlin code when extensions are used.

Try not to expose tuples as return types for public functions, try to minimize the scope in which properties are unnamed

Every now and then, you can see an API that exposes Triple<String, String, String> as its return type. However, this loses all semantic information on what each property actually is, and a data class with actual semantic names would be preferred.

Tuples without inline classes (which are experimental, so I personally don’t use them) should generally be resolved to its properties as soon as possible using positional decomposition.

combineTuple(name, password)
.observe(viewLifecycleOwner) { (name, password) ->
// ...
}

Avoid using pair.first and pair.second, always favor positional decomposition instead. If only one of the properties are needed, then you can do:

val (_, password) = tuple

Try to minimize the number of times you need to use safe-calls by disallowing receiving a nullable type in the first place

It’s very common to see code that reads like a?.b?.c?.d?.e, even though only a is nullable (and sometimes, not even a is expected to be nullable, but it’s not annotated with any nullability annotations, and therefore auto-complete might complete them as nullable — for example, in the case of Android’s listeners).

If in this example, only a is nullable though, then a simple actual if-check removes the need for chaining the safe-call operator on the rest.

if (a != null) {
val x = a.b.c.d.e // safe-cast
}

(Although you generally shouldn’t need to access so deeply into a class! Still, reducing the number of safe-calls can help readability).

In general though, you should aim to reduce the amount of nullable variables, if able. A function should avoid having to accept a nullable argument if it actually just chooses to ignore it, and other assertions such as checkNotNull(a) (or if you’re really sure, then even !! — but sparingly) can be used if the value truly should never be null.

What you ALWAYS want to avoid, is receiving a nullable type in a function, then immediately calling !! on it. That provides a false sense of security, and undermines typed nullability.

If you find yourself having to write a lot of loops over collections and to do list operations, consider checking the docs if there’s already an existing collection function with that given behavior

It’s very common to see loops for simple existing functions like List<T>.joinToString() , List<T>.any { condition }, List<T>.all { condition }, and sometimes even for map {} or filter {}.

There are numerous extension functions like take(), drop(), sumBy {} orfirstOrNull {} — but sometimes even groupBy {} or associateBy {} can help. In this case, it might not be worth writing the loops, when there’s already a function built in with equivalent behavior.

Don’t abuse infix functions, consider if a function truly needs to be infix

Infix functions allow us to ditch the .() when calling a function. However, this can make code more cryptic if overused.

Consider whether you need an interface, a higher-order function type, or a fun interface

Sometimes, giving a function a name is useful — in this case, both interface and fun interface can help. However, it is worth considering for callbacks (or especially mappers!) if a mere function type would be sufficient. After all, every Mapper<R, T> is actually just (T) -> R.

But if you do need the name, and you have only 1 function, consider using fun interface. If you need more functions, consider using a regular interface.

Conclusion

Hopefully that helped provide some insight over what it might be worth looking out for while working with Kotlin code, in order to improve it.

I had originally written a guide in 2018 for people who intended to move from Java to Kotlin, but it’s always nice to provide some refreshers!

Did I miss anything important? Don’t forget to leave your comments below.

--

--

Android dev. Zhuinden, or EpicPandaForce @ SO. Extension function fan #Kotlin, dislikes multiple Activities/Fragment backstack.