DEV Community

Rishabh Garg
Rishabh Garg

Posted on

Kotlin best practices

"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 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 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. 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 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.

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 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)
 } 
Enter fullscreen mode Exit fullscreen mode

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 } 
Enter fullscreen mode Exit fullscreen mode

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)
 } 
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)