DEV Community

Cover image for Java on Android — less sugar, more fun
Thomas Künneth
Thomas Künneth

Posted on

Java on Android — less sugar, more fun

Android 14 contains partial support for Java 17. For example, you can use the new language features sealed classes and multiline string literals. A couple of API additions have also been ported, like String.stripIndent() and String.formatted(). Android 15 will be getting even more Java 17 API (i.e. class library) goodness. For example, the first developer preview included package java.util.random. And with DP2, good old java.util.Collections gets the new method void shuffle(List<?>, RandomGenerator) (class RandomGenerator is part of that package). While this certainly is good news, adoption of newer Java classes and methods in Android apps has traditionally been slow. Why is that?

Changes to the Android class library are bound to the API level. If we want to use package java.util.random we need to make sure that our app is running on a device with at least API level 35 because otherwise users would experience crashes. But adding version branches bloats the app code, thus makes it harder to maintain. Consider this: what happens if we increase targetSdk version? At some point, the branches become useless, so we should likely remove them. Also, adding version branches begs the question what to do if the Android version is not new enough. That's why a lot of platform features can be used through ...Compat libraries. They invoke the platform code if the API level of the device running the app is high enough and provide alternative implementations for lower platform versions. So, should we look for a JavaStandardLibraryCompat?

(API) Desugaring

While Android inherits a lot from Java, it does not have a Java Virtual Machine (JVM). Early Android versions used a runtime environment called Dalvik, which was replaced by the Android Runtime (ART) in Android 5 (by the way, ART was available as an experimental feature in Android 4.4). The input for both is a file format called Dalvik Executable (DEX). During builds, DEX files are created from Java class files by converting JVM instructions to similar Dalvik and ART instructions.

When Java receives new language features, this may or may not affect the JVM instruction set. For example, multiline string literals (Java 13 to 15) are just syntactic sugar. They can be handled entirely by the Java compiler. In the end it will be one String regardless of its representation in the source code. However, lambda expressions (which were introduced in Java 8) use invokedynamic. This instruction was added in Java 7. To support Java class files that contain it (i.e. being able to execute code that uses the instruction), the Android toolchain needed to be updated accordingly (by producing DEX instructions that implement the mechanics of invokedynamic). Letting the toolchain do the conversion has one very important benefit: it is independent from API levels. The d8 tool can produce DEX code that will run on any Android version.

Wouldn't it be cool to get something similar for class library enhancements? Well, we already can. Not to much surprise, adding this was just the natural next step after providing modern Java language features to Android. When the Android Gradle Plugin 3.0 was released back in October 2017, we got lambda expressions, method references, type annotations, default and static interface methods, repeating annotations, and try with resources (a Java 7 feature) for all API levels. But no Java 8 APIs. A subset of them became available with AGP 4.0 in April 2020. Finally, AGP 7.4 (January 2023) adds support for a selection of Java 11 APIs.

Setup

To enable API desugaring, please set isCoreLibraryDesugaringEnabled to true and add a coreLibraryDesugaring dependency to your app module build.gradle or build.gradle.kts file. The library comes in three flavors. Click on the links to see which packages are supported.

Here's an example:

plugins {
  id("com.android.application")
  id("org.jetbrains.kotlin.android")
}

android {
  defaultConfig {
    
    // Required when setting minSdkVersion to 20 or lower
    multiDexEnabled = true
  }
  compileOptions {
    
    isCoreLibraryDesugaringEnabled = true
    sourceCompatibility = JavaVersion.VERSION_17
    targetCompatibility = JavaVersion.VERSION_17
  }
  kotlinOptions {
    
    jvmTarget = "17"
  }
  
}

dependencies {
  coreLibraryDesugaring(
    "com.android.tools:desugar_jdk_libs:2.0.4")
  
}
Enter fullscreen mode Exit fullscreen mode

What if

Setting API desugaring up is just a matter of minutes but allows you to use modern APIs in your Android app. However, you don't get all recent classes and packages. If you want to use java.util.random, you will be back to API level checking for now.

ART became a Project Mainline module in Android 12. This allows Google to update the Android Runtime through Google Play. If the Java Class Library was part of this Mainline module, the library could be refreshed independently from the Android framework. Apps would see the new version while the rest of the operating system uses the version it was shipped with.

There certainly are a few consequences: an app would still need to check the library version. Given ART is a crucial module I believe it would be ok the rely on a minimum version and ask the user to update ART if needed. But what is the library version? On the JVM you could invoke System.getProperty("java.specification.version") and would receive, for example 17. If you do this on Android, you will see 0.9 instead. This is documented here.

Certainly, there are reasons why Google may have decided not to follow this part. For example, size: undoubtedly, the Java Standard Library is a huge component. I still like the idea because adding another build time dependency affects the build time. Also, bugs in the library can be fixed through an OTA update.

What do you think? Please share your thoughts in the comments.

Top comments (0)