We all love Kotlin, Google has chosen it as the first-class language for Android development and the ASOS Android team consider it the only language for new feature implementation.
Since Kotlin is so good, it begs the question of whether we need to consider Java any more? Google has a specific page regarding the Java 8 support on Android Studio and Jake Wharton has written a useful blog piece regarding Java 8 support on Android. With the D8 desugars, Java 8 features lambda, default method and method references which are fully supported for all Android API versions. But some Java 8 features like Stream, Time etc are only supported for Android version 24+. Why do you need to consider the different support for Java 7 and Java 8 on Android if you’re using Kotlin?
Recently we had an issue when running Kotlin collection methods on devices running below Android 24. Here’s a code snippet.
As you can see, the code is very simple; it will return the default resource if the user hasn’t previously set any payment methods. When we ran the code, it passed our UI tests (Our test devices are all 24+) and Unit tests (Java 8). But when we ran the app on a device below 24, it throws a NoSuchMethod exception.
java.lang.NoSuchMethodError: No virtual method getOrDefault(Ljava/lang/Object;Ljava/lang/Object;)Ljava/lang/Object; in class Ljava/util/EnumMap; or its super classes (declaration of 'java.util.EnumMap' appears in /system/framework/core-libart.jar)
Thankfully the pre-launch test on the Play store identified this issue and sent us an alert.
Why did this error arise given we were using Kotlin? If you check the API document or the source code in Kotlin Github, you’ll find the annotation
PlatformDependent and target JDK 1.8 for
getOrDefaultmethod. When you decompile the Kotlin code, you can see the method java.util.EnumMap.getOrDefault is used.
The fix is really simple, you can either use Elvis operator
resources[paymentType] ?: default
Or, if you want to use a Kotlin method:
getOrElse is the same as using Elvis operator. The auto Kotlin converter in Android studio also converts the
getOrDefaultmethod using Elvis operator.
public inline fun <K, V> Map<K, V>.getOrElse(key: K, defaultValue: () -> V): V = get(key) ?: defaultValue()
Fixing the issue is simple, but why is the code platform dependent in the first place? From the document, we can see the platform dependent annotation specifies:
the corresponding built-in method exists depending on platform. Current implementation for JVM looks whether method with same JVM descriptor exists in the module JDK.
If you check the Collection class in Kotlin, two methods are annotated as
getOrDefault(key, default) and
remove(key, value). They are both introduced from Java 8 . Android’s support for this bytecode requires a minimum API of 24 but it will give a lint check warning if your minimum version is below 24. Here’s a very simple snip to call these methods directly in the Java class and the lint check which will show the warning directly in the Android Studio.
When you call it from Kotlin, it will show the same warning if it’s a
HashMap class, but it doesn’t show the same warning when you call from an
TreeMap. That’s why our CI system doesn’t report this issue.
What can we do to prevent this issue from happening in the future? Pay more attention to the Kotlin collection classes when using them on Android and always test your code on devices running the minimum Android version you supported. In our CI process, we are trying to add minimum support devices to catch the issue in the pull request review state and have nightly build releases in the internal test channel to make full use of the pre-launch test.
My name is Peng Jiang, I’m a Lead Android Engineer at ASOS and am passionate about simple and well-crafted code. In my spare time, I also play with Swift and Flutter. At ASOS, we’re always looking for strong, friendly and talented developers. You’ll only need to write Kotlin here. If that sounds interesting to you, do get in touch!