Enjoying legacy - modern Java in Android apps

tkuenneth

Thomas Künneth

Posted on August 9, 2021

Enjoying legacy - modern Java in Android apps

Kotlin is the preferred language for Android apps since 2019. Two years earlier Google announced support for Kotlin in Android Studio. Quite some time has passed since then. So, why does an article from summer 2021 still deal with modern Java in Android apps?

Because Java is still present in many Android apps. Particularly in those released before 2017. Sure, many have been partially or fulled converted to Kotlin. But others haven't. And, the older an app is, the older is the Java flavor being used. Apache Harmony (whose class library made its way into Android) started as Java 1.5. So, very old apps look like that: tons of anonymous inner classes. It took quite some time before Java 7 features like Strings in switch statements or try-with-resources and AutoCloseable became available. Lambdas and streams of Java 8 weren't available immediately, either.

So, Java code in Android apps probably looks older than it needs to. We could of course say, as long as that code causes no trouble, we best leave it alone. But if it is still a vital part of the app we should treat it like we treat new code: care for it. This can mean:

  • convert it to Kotlin
  • make use of modern Java features

I am not going to compare these approaches, as this would be an article in its own right. 😀 Instead I will focus on the latter option. But before we dive in: this is not about Java vs. Kotlin, and this is certainly not about debating which programming language is better. I just think that any code should be as good as possible.

Setting things up

Language versions are configured in the module-level build.gradle file.

android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_11
    targetCompatibility JavaVersion.VERSION_11
  }
  kotlinOptions {
    jvmTarget = '11'
    useIR = true
  }
  ...
Enter fullscreen mode Exit fullscreen mode

To use Java 11 we also need to configure the Android Gradle plugin 7.0, which, by the way requires Java 11. This is done in the project-level build.gradle file.

buildscript {
  ...
  repositories {
    google()
    mavenCentral()
  }
  dependencies {
    classpath "com.android.tools.build:gradle:7.0.0"
    ...
Enter fullscreen mode Exit fullscreen mode

But why Java 11? Didn't we see Java 16 in March? Java 11 is the first long-term support (LTS) release after Java 8. Oracle stopped supporting Java 8 in January 2019. The next LTS-version will be Java 17, which is scheduled for September 2021.

What's in stock?

As Kotliners we love to write something like

var s = resources.getString(R.string.app_name)
Enter fullscreen mode Exit fullscreen mode

Or val, if s is not gong to change.

This is possible since Java 10, too:

var s = context.getString(R.string.app_name);
Enter fullscreen mode Exit fullscreen mode

You can make s final if needed. What we see here is called local type inference. Nice, isn't it? Please keep in mind, though, that it doesn't work outside of blocks, methods, and constructors.

Java 11 adds var for lambda parameters.

var list = Arrays.asList("Hello", "Android");
var result = list.stream()
    .map((var x) -> x.toUpperCase(Locale.getDefault()))
    .collect(Collectors.joining(", "));
Enter fullscreen mode Exit fullscreen mode

There are also some new methods to String: isBlank(), lines(), strip(), stripLeading(), stripTrailing(), and repeat(). Sadly though, we can't use them (at the time of writing). That is because the Android class library is a combination of the Java class library and Android-specific packages. Changes to the OpenJDK class library do not become part of Android automatically. Instead they are hand-picked, sort of. Practically every API level sees some changes being ported. For example, with API level 31 we will get compareUnsigned() in java.lang.Short. It debuted in Java 9.

Even older is try-with-resources: one of the coolest, and possibly least used features of Java 7. Take a look:

FileInputStream fis = null;
try {
  fis = context.openFileInput("myfile.txt");
  // do some work
} catch (FileNotFoundException e) {
  Log.e(TAG, "something went wrong", e);
}
if (fis != null) {
  try {
    fis.close();
  } catch (IOException e) {
    Log.e(TAG, "something went wrong", e);
  }
}
Enter fullscreen mode Exit fullscreen mode

Yes, this screams boilerplate code. But how about this?

try (FileInputStream fis = context.openFileInput("myfile.txt")) {
  // do some work
} catch (IOException e) {
  Log.e(TAG, "something went wrong", e);
}
Enter fullscreen mode Exit fullscreen mode

In case you are wondering... No, I didn't forget about the close(). This is done automatically. Next...

try {
  var fis = context.openFileInput("myfile.txt");
  // do some work
} catch (IOException e) {
  Log.e(TAG, "something went wrong", e);
} catch (IllegalArgumentException e) {
  Log.e(TAG, "something went wrong", e);
} catch (NullPointerException e) {
  Log.e(TAG, "something went wrong", e);
}
Enter fullscreen mode Exit fullscreen mode

Yes, here definitely is something wrong.

Why not this way?

try {
  var fis = context.openFileInput("myfile.txt");
} catch (IOException |
    IllegalArgumentException |
    NullPointerException e) {
  Log.e(TAG, "something went wrong", e);
}
Enter fullscreen mode Exit fullscreen mode

Since Java 7 a single catch block can handle more than one type of exception. This can reduce code duplication. And you no longer have an explanation for catching Throwable. 🤣

Another source of boiler plate code are anonymous inner classes

Button b = findViewById(R.id.button);
b.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
    // do some work
  }
});
Enter fullscreen mode Exit fullscreen mode

We can shrink this to:

Button b = findViewById(R.id.button);
b.setOnClickListener(view -> {
  // do some work
});
Enter fullscreen mode Exit fullscreen mode

As you have seen, some of the prejudices Java is still facing are (to some extent) no longer valid. It is up to developers to modernize their code. There's quite a lot you can do.

Conclusion

Modernizing old Java code is undoubtedly worth the effort. First, it becomes much easier to read. Second, you will be able to remove tons of boilerplate code. And third, your app will become less error-prone. Don't believe me? Be honest, does your code really put close() inside try-catch? ...well, you should. And with try-with-resources you get it for free.

💖 💪 🙅 🚩
tkuenneth
Thomas Künneth

Posted on August 9, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related