Blog Post

Java 8 in Android Apps

Illustration: Java 8 in Android Apps

Android Studio 3.0 introduced support for a subset of Java 8 language features and APIs for Android apps. This article gives an overview of the available Java 8 language features and describes how to enable them in your Android apps.

How It Works

Java 8 has been supported natively since Android SDK 26. If you wish to use Java 8 language features and your minimal SDK version is lower than 26, .class files produced by the javac compiler need to be converted to bytecode that is supported by these SDK versions. This conversion process is called desugaring. Desugaring must be enabled for all modules that use Java 8. Moreover, it must be enabled even when some of a module’s transitive dependencies use Java 8.

Desugaring does not support all Java 8 features on all Android versions. Generally speaking, new language features — such as lambda expressions and method references — are available on all SDK versions, whereas the new Java 8 APIs — such as Streams — are available only on Android versions that support them natively.

Enabling Java 8

Java 8 has been supported since Android Studio 3.0 and Android Gradle plugin version 3.0.0. To enable Java 8 in your apps, you’ll need to update your Android Studio and Android Gradle plugin to meet these minimal version requirements:

buildscript {
    repositories {
        google()
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:3.3.0'
    }
}

Then update the Source and Target Compatibility to 1.8 for each module that will use Java 8:

android {
  ...
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

ℹ️ Note: You’ll need to enable Java 8 even for modules where Java 8 is used only by the modules’ transitive dependencies. Otherwise, when compiling their code, you’ll get exceptions similar to what’s shown below.

D8: Invoke-customs are only supported starting with Android O (--min-api 26)
Caused by: com.android.builder.dexing.DexArchiveBuilderException: Error while dexing.
The dependency contains Java 8 bytecode. Please enable desugaring by adding the following to build.gradle
android {
    compileOptions {
        sourceCompatibility 1.8
        targetCompatibility 1.8
    }
}
See https://developer.android.com/studio/write/java8-support.html for details. Alternatively, increase the minSdkVersion to 26 or above.

Migrating from Other Toolchains

If you are currently using another toolchain — such as Retrolambda — that supports some of the Java 8 features, Gradle and Android Studio will fall back to Java 8 support provided by these tools. Additionally, if you are using the Jack toolchain that already has support for most Java 8 features, you should migrate to the default toolchain because Jack has been deprecated and will no longer be updated.

Lambda Expressions

One of the most useful Java 8 language features is that of lambda expressions. When you look at typical Java code, you’ll immediately notice a number of anonymous classes that implement interfaces containing only a single method. The syntax for these classes is just boilerplate that does not provide any additional information for the reader. Button click listeners are a good example of this:

button.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View view) {
        // Handle button clicks.
        ...
    }
});

Lambda expressions let you replace these anonymous classes in a more compact and readable manner:

button.setOnClickListener(view -> {
    // Handle button clicks.
    ...
});

Lambda expressions are also a great way to improve the readability of code that uses higher-order functions such as map or filter:

// You can define functions with lambdas and pass them as parameters to higher-order functions.
Predicate<Integer> isPositive = value -> value > 0;
Function<Integer, Integer> square = value -> value * value;

Single<List<Integer>> listSingle = observable
        .filter(isPositive)
        .map(square)
        // Continue with the RxJava chain.
        ...

Android Studio provides handy inspections for converting existing anonymous classes to lambdas. Simply press Alt + Enter inside an anonymous class declaration and choose Replace with lambda, as shown below.

Replace with lambda

Syntax

A lambda expression starts with a comma-separated list of parameters enclosed in parentheses. If there is only one parameter, parentheses are not required:

// Parameters are enclosed in parentheses.
(parameter1, parameter2) -> {
    // Lambda body
}

// Parentheses can be omitted if there is only one parameter.
parameter -> {
    // Lambda body
}

Parameters are followed by the arrow token (->) and a body. The body of the lambda consists of a single expression or code block enclosed in curly braces. In case of a single expression, the Java runtime evaluates the expression and returns its value:

// The body as a code block.
value -> {
    Log.d(LOG_TAG, "Filter usage for value %s", value);
    return value > 0;
}

// The body can also be written as a single expression.
p -> value > 0;

Capturing Local Variables of the Enclosing Scope

Similar to anonymous classes, lambda expressions capture variables from the scope. However, only final or effectively final variables can be accessed:

void function(int x) {
  Predicate<Integer> predicate = (value) -> {
      // The following statement would result in an error because the local
      // variable x referenced in lambda is no longer effectively final:
      //
      // x = 10;

      // Unlike with anonymous classes, it is not possible to shadow variables
      // from an enclosing scope in a lambda expression. For example, the following
      // also produces an error:
      //
      // int x = 10;

      return x > 0;
  }
}

Method References

A lambda expression sometimes just calls an existing method. Method references, as their name suggests, enable you to reference these methods by name instead of by using lambdas.

Consider the following method:

public class MathFilters {
  public static boolean isPositive(int x) {
      return x > 0;
  }
}

We can use this method in a lambda expression:

observable.filter(value -> MathFilters.isPositive(value));

However, using the method reference is cleaner and more readable:

observable.filter(MathFilters::isPositive);

Default and Static Interface Methods

Since Java 8, interfaces can define default implementations for their methods, and they can even define static methods. The former is handy when introducing new methods to an existing interface without modifying any of the classes that implement this interface. The latter allows you to introduce helper methods into your interfaces instead of introducing separate classes.

For example, consider the following interface for custom loggers:

public interface Logger {
    void log(int priority, String tag, String message, Throwable throwable);
}

We want to add a new method to filter logs before they are logged:

public interface Logger {
    boolean isLogged(int priority, String tag);
    void log(int priority, String tag, String message, Throwable throwable);
}

However, this would mean that all classes that are already implementing this interface need to provide implementation of this method. We can solve this by providing the default implementation for the isLogged() method:

public interface Logger {
    default boolean isLogged(int priority, String tag) {
        // Accept all logs by default.
        return true;
    }

    void log(int priority, String tag, String message, Throwable throwable);
}

Now let’s see an example of the static method in an interface. Consider the following helper class that provides static factory methods for various loggers:

public class Loggers {
    public static Logger createLogCatLogger() {
        return new LogCatLogger();
    }
    ...
}

These methods can be moved to the Logger interface when using Java 8:

public interface Logger {
    ...

    static Logger createLogCatLogger() {
        return new LogCatLogger();
    }
}

Annotation Improvements

Java 8 also brings with it some improvements on the annotation front, namely that annotations can now be applied to any type use:

// Object creation.
new @Internal MyObject();

// Type casts.
nonNullString = (@NonNull String) str;

// Class implements clauses.
class ImmutableList<T> implements @Readonly List<@Readonly T> { ... }

// Throws declaration.
void function() throws @Critical Exception { ... }

Another nice addition is that it is now possible to use multiple annotations at the same time:

@Author(name = "John Appleseed")
@Author(name = "John Doe")
class Foo { ... }

New Java 8 Language APIs

The language features discussed earlier in this post are supported on all Android versions thanks to the desugaring process. Java 8 is not only about language features though; it also brings some new APIs to the table. However, these are not useful to most apps, as they require API level 24 or higher.

The new APIs mostly complement the new features. For example, the introduction of lambda expressions brought with it an API that allows functional-style operations (streams and functional interfaces). And annotation improvements and default interface methods introduced new reflection APIs for working with the language features.

Conclusion

The support for Java 8 that was introduced in Android Studio 3.0 means a lot to developers like us here at PSPDFKit, as we can’t fully switch to Kotlin just yet. We introduced Java 8 to our codebase in version 5.0, just a few months ago, and we can already see how the new features (especially lambdas) improve our day-to-day coding experience and make the code easier to read and understand.

Since the Java 8 source compatibility is mandatory for all modules with dependencies that are using Java 8, now is the right time to consider enabling it in your codebase before you are forced to do so because some of your important dependencies start using Java 8.

Author
Tomáš Šurín Server and Services Engineer

Tomáš has a deep interest in building (and breaking) stuff both in the digital and physical world. In his spare time, you’ll find him relaxing off the grid, cooking good food, playing board games, and discussing science and philosophy.

Share Post
Free 60-Day Trial Try PSPDFKit in your app today.
Free Trial

Related Articles

Explore more
DEVELOPMENT  |  Android • Jetpack Compose

How to Implement Drag-to-Reorder List Functionality with Jetpack Compose

DEVELOPMENT  |  iOS • Android • Room • Kotlin Multiplatform • Tips

Seamless Room Database Integration for Kotlin Multiplatform Projects

PRODUCTS  |  Android • Releases

Android 2024.1 Update: Advanced Content Editing and Digital Signatures, Plus Expanded Jetpack Compose Support