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
file → FactoryKt
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.