Where Should I Keep My Constants in Kotlin?

There has recently been a discussion on the ASG Slack #kotlin channel about the best approach for storing global constants, or public static final fields in Javaspeak. This short article describes available options and points out some of the pitfalls you can run into. But before we start, let's talk a bit about decompiling Kotlin code.

Decompiling Kotlin

If you've used Kotlin for some time you should've noticed that the language is a wonderful boilerplate reduction tool. Kotlin makes it easy to express complex things with simple code, with compiler doing the dirty work. A great example is the data class feature, which allows you to easily substitute tens of lines of Java code with just a single line of Kotlin. However, as we all know, with great power comes great responsibility. It's not hard to make Kotlin compiler produce suboptimal bytecode, and, especially if you're doing Kotlin for Android, you have to be aware of the number of classes, methods and object allocations that your code will produce. Luckily, JetBrains has us covered with a decompiler tool integrated into Android Studio (and IntelliJ IDEA of course), that helps examine the bytecode, and even produces similar Java code. The latter makes it easy to optimize Kotlin.

There's a number of great resources online on the topic of decompiling Kotlin, so I won't go into much detail here and will just share some links:

Let's now jump straight to the main topic: the constants.

Constants in Kotlin

Companion objects

There's no static keyword in Kotlin. If you want static access to certain fields or methods of your class, you should put them under a companion object. A naive way to declare a constant would look like this:

class Constants {  
  companion object {
    val FOO = "foo"
  }
}

The field will be available globally and accessible through Constants.FOO. But let's now use the decompiler and see how this code would look if written in Java (simplified variant):

public final class Constants {  
   @NotNull
   private static final String FOO = "foo";
   public static final Constants.Companion Companion = new Constants.Companion((DefaultConstructorMarker)null);

   public static final class Companion {
      @NotNull
      public final String getFOO() {
         return Constants.FOO;
      }

      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

It's important to note here that a companion object is an actual object: Kotlin's Constants.FOO call will translate into Java's Constants.Companion.getFOO(). This version is pretty bad, as it introduces an object and a method that could've been avoided.

const vals

A simple way to improve our example is to mark FOO as const:

class Constants {  
  companion object {
    const val FOO = "foo"
  }
}

Here's the Java version:

public final class Constants {  
   @NotNull
   public static final String FOO = "foo";
   public static final Constants.Companion Companion = new Constants.Companion((DefaultConstructorMarker)null);

   public static final class Companion {
      private Companion() {
      }

      // $FF: synthetic method
      public Companion(DefaultConstructorMarker $constructor_marker) {
         this();
      }
   }
}

The getter is gone, and we actually have direct static access to the field now, but we still have a useless companion object generated by the compiler. Another thing to note about const is that it only works with primitives and Strings:

class Constants {  
  companion object {
    // won't compile
    const val FOO = Foo()
  }
}

A workaround is to use the @JvmField annotation on the val:

class Constants {  
  companion object {
    @JvmField val FOO = Foo()
  }
}

This will make the Foo instance public static final. There's an important difference between the behavior of const and @JvmField: accesses to a const val get inlined by the compiler, while @JvmFields don't. Let's take the following code:

fun main(args: Array<String>) {  
  println(Constants.FOO)
}

Here's what we get with @JvmField val FOO = Foo():

public final class MainKt {  
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      Foo var1 = Constants.FOO;
      System.out.println(var1);
   }
}

and with const val FOO = "foo":

public final class MainKt {  
   public static final void main(@NotNull String[] args) {
      Intrinsics.checkParameterIsNotNull(args, "args");
      String var1 = "foo";
      System.out.println(var1);
   }
}

There's no call to Constants.FOO in the second example, the value has been inlined.

Dropping the class and the object

If all we need is just a set of constants - we can safely drop both the class and the object and use top-level vals:

const val FOO = "foo"  

The result is exactly as you would've written it in Java:

public final class ConstantsKt {  
   @NotNull
   public static final String FOO = "foo";
}

In Kotlin, you'll be accessing the value by its name globally. If you're using the value in Java code, you'll call ConstantsKt.FOO. To avoid Kt suffixes on class names, use the file:@JvmName annotation on top of the file to specify a more readable name:

@file:JvmName("Constants")

Compiler will use provided value for the class name:

public final class Constants {  
   @NotNull
   public static final String FOO = "foo";
}

Conclusion

Even though there's no static keyword in Kotlin, it's easy to define constants that are globally accessible. It's also quite easy to get this wrong and introduce redundant methods and object allocations into the bytecode. The decompiler tool can help you locate and fix this kind of issues, and comes with an additional benefit - you'll quickly learn how all that Kotlin magic works under the hood! Have fun!

If you've found this article helpful - please make sure to share it with your subscribers using one of the links below. Feedback is always appreciated, so please feel free to share your thoughts in the comments.