Kotlin: Is It Possible to Overload Top-Level Functions in Different Modules?

Jan 23, 2021

For libraries, I like to separate the Android-specific and the plain JVM part into different modules. The reason for this is better testing tools and better reusability of the JVM part. But that’s a different topic. I discovered an interesting behavior when creating top-level functions in different modules that still live in the same package. The setup looked like this:

┌─ mylib-core
│  └─ src/main/java/at/xa1/example
│     ├─ Factory.kt
│     ├─ StringText.kt
│     └─ Text.kt
└─ mylib-android
   └─ src/main/java/at/xa1/example
      ├─ Factory.kt
      └─ AndroidStringResourceText.kt

Only the two Factory.kt files are relevant for now:

mylib-core/…/Factory.kt:

package at.xa1.example

fun Text(text: String): Text = StringText(text)

mylib-android/…/Factory.kt:

package at.xa1.example

import androidx.annotation.StringRes

fun Text(@StringRes stringResId: Int): Text = AndroidStringResourceText(stringResId)

Text is an overloaded top-level factory function that can either take a String or a string resource Int to construct an instance of a class that implements a generic Text interface:

val stringText = Text("example")
val androidText = Text(R.string.example)

However, when doing this the compiler complains:

e: mylib-android/src/test/java/at/xa1/example/TextTest.kt: (12, 31):
Type mismatch: inferred type is String but Int was expected

It looks like the compiler only finds the Text(Int) function but not the Text(String). Why is this? When putting both functions into different packages, everything works as expected. But then an additional import is required, which is not so nice from a library usability perspective.

Solution

The easiest solution is to simply rename one of the files. E.g. mylib-android/…/Factory.kt could be renamed to mylib-android/…/FactoryAndroid.kt, but the filename of mylib-core/…/Factory.kt remains unchanged. After the file rename the code works as expected, without an additional import and without renaming the functions themselves.

The background is that Kotlin compiles to the JVM. In Java (JVM) there are no top-level functions. All methods have to be defined in a class. To work around that limitation the Kotlin compiler will create a new class that contains the Kotlin top-level function. All of this happens under the hood of the Kotlin compiler. You can see the result in IntelliJ or Android Studio by clicking Tools › Kotlin › Show Kotlin Bytecode › Decompile:

public final class FactoryKt {
   @NotNull
   public static final Text Text(@NotNull String text) {
      Intrinsics.checkNotNullParameter(text, "text");
      return (Text)(new StringText(text));
   }
}

The compiler creates a class with the same name as the file containing the top-level function: Factory.kt fileFactoryKt class.

So far so good, the Kotlin compiler will create a FactoryKt class with the top-level function for each module. And there is where another JVM limitation strikes: In a JVM process, each fully qualified name (package name + class name) must be unique.

The compiler error that we saw before surfaces because now there are two classes with the fully qualified name at.xa1.example.FactoryKt. Since there can only be one, the compiler takes one and ignores the other. From the compiler’s perspective, there is only one Text function. Being only aware of one Text function also explains the quite misleading error message.

When renaming one of the Factory.kt files, the compiler will generate two different class names under the hood that don’t have a name collision. As a result, the compiler is aware of both Text functions and everything works fine.

Update 2022-05-15

Alternatively, it’s possible to name the generated class without renaming the file:

@file:JvmName("FactoryAndroid")

Add this annotation at the very top of the file. You can choose any name that doesn’t cause a name collision.

Conclusion

From a Kotlin perspective, we don’t really care about the generated classes. In this case, however, background knowledge is necessary to resolve the issue. A phenomenon Joel Spolsky named The Law of Leaky Abstractions. I hope this explanation was helpful, even if you don’t have exactly this problem. If you’re missing some details, let me know.

You might also like