Kotlin Constants in Android: Top-level vs. Companion-enclosed
Background
We often declare Kotlin constants in Android without giving them a second thought. Some use top-level constants while others wrap them inside a companion object. But does this choice affect performance, APK size, or memory usage? Let’s break it down by diving into the bytecode and R8 optimizations.
Preparing the Code
const val TOP_LEVEL_CONSTANT = "TOP_LEVEL_CONSTANT"
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
println(TOP_LEVEL_CONSTANT)
println(COMPANION_ENCLOSED_CONSTANT)
}
companion object {
const val COMPANION_ENCLOSED_CONSTANT = "COMPANION_ENCLOSED_CONSTANT"
}
}
We have top-level constant at the top and a companion object containing a constant at the bottom of MainActivity
. The constants are used in onCreate
.
Decompiling the Class With Android Studio
To get the Java bytecode, we’ll simply go to the Tools
menu in Android Studio -> Kotlin
-> Show Kotlin Bytecode
. A new pane showing Java bytecode will appear on the right.
Since reading Java bytecode directly is complex, we’ll click the Decompile
button in the pane to get a Java representation.
public final class MainActivity extends ComponentActivity {
@NotNull
public static final Companion Companion = new Companion((DefaultConstructorMarker)null);
@NotNull
public static final String COMPANION_ENCLOSED_CONSTANT = "COMPANION_ENCLOSED_CONSTANT";
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String var2 = "TOP_LEVEL_CONSTANT";
System.out.println(var2);
var2 = "COMPANION_ENCLOSED_CONSTANT";
System.out.println(var2);
}
public static final class Companion {
private Companion() {
}
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
public final class MainActivityKt {
@NotNull
public static final String TOP_LEVEL_CONSTANT = "TOP_LEVEL_CONSTANT";
}
I removed the parts that aren’t relevant to our discussion to make it easy to read. Let’s break down the relevant parts.
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String var2 = "TOP_LEVEL_CONSTANT";
System.out.println(var2);
var2 = "COMPANION_ENCLOSED_CONSTANT";
System.out.println(var2);
}
Inside the onCreate
method, we can see that the constants are inlined and that’s great.
public final class MainActivityKt {
@NotNull
public static final String TOP_LEVEL_CONSTANT = "TOP_LEVEL_CONSTANT";
}
However, now we end up with a redundant MainActivityKt
class that isn’t even used. This comes from the top-level constant and it increases the app size.
public final class MainActivity extends ComponentActivity {
@NotNull
public static final Companion Companion = new Companion((DefaultConstructorMarker)null);
@NotNull
public static final String COMPANION_ENCLOSED_CONSTANT = "COMPANION_ENCLOSED_CONSTANT";
...
public static final class Companion {
private Companion() {
}
public Companion(DefaultConstructorMarker $constructor_marker) {
this();
}
}
}
With a companion-enclosed constant, it’s even worse. The companion object becomes a static inner class which gets instantiated during the outer class loading and assigned to a static field. This introduces unnecessary object creation and memory overhead.
Relevance of D8 and R8
It’s important to note that the Java code we looked at came from Java bytecode and Android does not run Java bytecode. The Java bytecode still needs to go through a dex compiler called D8 to turn into Dalvik bytecode that Android can run [1][2]. More importantly, our code hasn’t gone through R8 optimization yet. R8 optimization will remove unused code [3].
Code shrinking (also known as tree shaking), is the process of removing code that R8 determines is not required at runtime. This process can greatly reduce your app’s size if, for example, your app includes many library dependencies but utilizes only a small part of their functionality.
So, to apply R8, we can enable it in the project-level Gradle file (also known as root-level Gradle file).
isMinifyEnabled = true
Next, we go to the Build
menu of the Android Studio -> Build APK(s)
to build an APK that contains Dalvik bytecode.
Decompiling the APK With Apktool and Jadx
Now that we got our APK ready, let’s see what happens when D8 and R8 are involved in the compilation process.
So, to get the Dalvik bytecode from the APK, we’ll use apktool [4].

We get these two files. Let’s see what’s inside each of them but first, to simplify our analysis, we need to translate the Dalvik bytecode into Java representation. (If you wanna check the Dalvik bytecode, I’ve posted them here.)
The easiest way to get the Java representation from smali files is to use jadx [5].
public final class MainActivity$Companion {
public /* synthetic */ MainActivity$Companion(DefaultConstructorMarker defaultConstructorMarker) {
this();
}
private MainActivity$Companion() {
}
}
This is what we get for MainActivity$Companion.smali
.
Looking at the code, we now know that R8 doesn’t remove the static inner class derived from the companion object but what happened to MainActivityKt
that comes from the top-level constant? Let’s look into MainActivity.smali
to find out.
public final class MainActivity extends ComponentActivity {
public static final Companion Companion = new Companion((DefaultConstructorMarker) null);
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
System.out.println((Object) "TOP_LEVEL_CONSTANT");
System.out.println((Object) "COMPANION_ENCLOSED_CONSTANT");
}
}
MainActivityKt
is not here either, so we can conclude that R8 removes it. This happens because nothing references MainActivityKt
. So, R8 knows it’s unused. As for the static inner class derived from the companion object, because it’s referenced by MainActivity
, R8 doesn’t remove it.
Disclaimer:
- please, note that different decompiler decompiles APKs differently. If you use jadx directly on the APK, it’ll remove the static inner class derived from the companion object too. This can make you think R8 removes it but the fact that apktool can still get it means it’s in the APK.
- I used Android Gradle Plugin 8.8.0. Different Android Gradle Plugins use different R8 versions and this may affect the result.
Sharing Constants Across Different Classes
Sometimes, you might wanna share the constants across different classes. For that, we can declare top-level constants in a dedicated file, now that we know classes generated for enclosing only constants are removed by R8 because they’re not referenced by any classes after the constants are inlined.
Grouping Multiple Constants for Readability
Sometimes, keeping all constants at one level is not enough. Sometimes, you might wanna group them for readability. Let’s see if grouping with singleton objects is a viable option.
object MyConstants {
const val FIRST_LEVEL = "FIRST_LEVEL"
object NestConstants {
const val SECOND_LEVEL = "SECOND_LEVEL"
}
}
So, we create the MyConstants
class and simply print the constants in MainActivity
.
We’ll also create HelloActivity
, declare it in the manifest and make it use FIRST_LEVEL
and SECOND_LEVEL
. (I’ll leave out the code for HelloActivity
to keep the article short.)
Then, we’ll build the APK and use apktool to decompile it.

This is all we get. MyConstants
is gone. R8 can remove it for the same reason it can remove MainActivityKt
. It’s because after inlining the constants from that class, the class is no longer referenced by any other class which helps R8 know it’s not used. (If you still wanna take a look at the smali files, I’ve posted them here.)
Conclusion
- Companion-enclosed Constants: Avoid them, as they generate redundant static inner class that introduces unnecessary object creation and memory overhead.
- Top-level Constants: Best for single-file usage.
- Shared Constants Across Multiple Classes: Use a dedicated file with top-level constants.
- Grouped Constants: Use singleton objects.
- R8 Optimization: Always enable
isMinifyEnabled
for production builds to benefit from code shrinking. R8 removes the enclosing classes of top-level, shared, and grouped constants because the classes are no longer referenced after the constants are inlined.
Happy coding, everyone!
References
- https://source.android.com/docs/core/runtime [Accessed on: 6 Feb 2025]
- https://developer.android.com/tools/d8 [Accessed on: 6 Feb 2025]
- https://developer.android.com/build/shrink-code [Accessed on: 6 Feb 2025]
- https://apktool.org/ [Accessed on: 6 Feb 2025]
- https://github.com/skylot/jadx [Accessed on: 6 Feb 2025]