Kotlin best practices
Tags: programming, tutorial, kotlin
Published: 2023-10-04 07:02:00



"With great power comes great responsibility"

Kotlin is a modern programming language with many benefits and a lot of convenient and powerful features. However, some of these features come with unexpected performance costs that we should be aware of and careful with.

## **General guidelines**

Do **NOT** :

- Use Hungarian notation.
- Use `apply`, `let`, `run`, etc unnecessarily, they reduce code readability. Usually unnecessary, but one good case is using them to avoid an explicit null check (e.g. x?.let { ... }).
- Use `String.format("%d", x)`, Kotlin supports inlining variables in strings directly: `"$x"`.
- Create private vars with a getter function. Instead, make a `public var + privateset`
- Use by lazy initialization. It has significant hidden performance costs. See [Lazy initialization](#_6imxm7s4mn8h) for more details.
- Use `!!` or `checkNotNull` unless required for legacy Java code (e.g. during Kotlin conversions, if using legacy Java APIs, etc.). It's one of the only ways to get a NPE in Kotlin. There is always a better way to organize the code to avoid nullability, or to use the Kotlin safe operator `(?.)`.
- Use `Dispatchers.Default orDispatchers.IO.` Instead, use our own `CoroutineDispatchers.Background`, the Kotlin ones use a different thread pool.
- Use `runBlocking` or `backgroundRunBlocking`, they will block the underlying thread causing performance issues.
- Create functions that take in lambda function arguments, without making the higher-order function inline. Not using inline can significantly affect performance. See [Higher-order functions](#_y5gv7nkufg9t) for more details.
- Mix Kotlin and Java in the same BUCK module. This is supported, but it can cause subtle bugs because of Java-Kotlin interoperability.

I pulled some of the above don'ts from a list of somewhat outdated dos and don'ts in [this Stella Kotlin guide](https://fburl.com/wiki/h0js36ka). Do take a look at the 'Common Patterns' section of that guide for some useful ways to avoid some of the patterns above.

## **Companion objects**

Companion objects provide an easy way to add static functions and variables to a class.

There used to be additional overhead introduced by companion objects, but it has since been [optimized by Redex](https://fburl.com/wiki/c2o8sp5b) so you are free to use them.

Use cases and best practices:

- Constants (const val)
  - Private constants (e.g. TAG strings): no need for a companion object, you can just define them outside the class.
  - Public constants: prefer placing them inside companion objects, so that when accessed from other files they are scoped to the class they are defined in.
- Static functions
  - If all functions are static, use object instead of class. Objects are equivalent to Java singletons, and are instantiated lazily on first access.
  - Otherwise place them inside a companion object (and add @JvmStatic if called from Java code)

## **Beware Kotlin objects**

Kotlin objects follow the singleton pattern. While these can be useful, we should never create **stateful** Kotlin objects:

- They can cause memory leaks given that their lifecycle is scoped to the application. For example, storing a Context variable in a Kotlin object can very easily leak an entire activity.
- They are very hard to unit test. This is because state is hard to reset between test runs, and dependencies are impossible to inject without workarounds.

Instead:

- Only create Kotlin objects to store **static functions and constants**.
- If you really need a singleton, create it manually – make your singleton a class and then store the singleton instance in a companion object, injecting any dependencies. Do not store Context variables or any other component that is not scoped to the application lifecycle. See [example](https://fburl.com/code/lpbmsf02).

## **Lazy initialization**

In Kotlin you can initialize a variable lazily / on first access using the lazy keyword. This is very convenient, but when compiled down to Java there is a lot of unexpected additional overhead. Read the Lazy Initialization section of [this wiki](https://fburl.com/wiki/4b0e6qdu) for a detailed explanation on this.

As a general rule, don't use it. However, the overhead can be worth it if the initialization of an object is particularly expensive and there's no other way to organize the code.

## **Higher-order functions and Lambda expressions**

Kotlin supports lambda expressions and storing functions in variables:

| val sum = { x: Int, y: Int -\> x + y } |
| --- |

You can also pass functions into other functions:


```
fundoOperation(a: Int, b: Int, operation: (Int, Int) -\> Int) 
{
return operation(a, b)
 } 
```


We can now pass `sum` into `doOperation`
 doOperation(2, 3, sum) 

You can also directly pass the lambda expression to the higher-order function:

```
doOperation(2, 3) { x, y -\> x + y } 
```

This is very useful to reduce boilerplate and improve code readability, but it comes at a performance cost. There is additional runtime overhead because each function argument becomes an object capturing a closure (the group of variables accessed in that function), requiring extra memory allocations and virtual calls.

To solve this, Kotlin offers the inline keyword:

```
inlinefundoOperation(a: Int, b: Int, operation: (Int, Int) -\> Int) {
return operation(a, b)
 } 
```

This inlines the function argument inside the higher-order function, removing the additional overhead. Note that this will increase code size, so as a general rule **avoid inlining large functions**.