Java 8 Lambda Expressions

In 2016, Android app development gained some ability to use Java 8 programming constructs.

One of those changes was the release of Android 7.0, which introduced some Java 8-compatible classes, such as those in the java.util.stream package. These are new to API Level 24. Most likely, you will only start to use them once you raise your minSdkVersion to 24 or higher.

However, some features can be used on older devices. Notable among these are lambda expressions, the Java 8 equivalent of blocks or closures that you find in other programming languages. Lambda expressions can make your code a bit less verbose, particularly in places where you are making heavy use of listener interfaces or other forms of callback objects.

Getting all this to work requires some Gradle changes (to request Java 8 support in the build process), plus using the new lambda expression syntax. This chapter will show you all of this.

Prerequisites

Understanding this chapter requires that you have read the core chapters of the book. It does not require that you have prior experience with Java 8 lambda expressions.

However, having read the chapter on RecyclerView is a good idea, as the sample app in this chapter was originally shown there.

The Basic Idea

In Java programming prior to Java 8, we needed to create a lot of classes for minor roles, whether those classes were traditional Java classes, nested classes, or anonymous inner classes.

For example, suppose that you have an ArrayList of Video objects:

package com.commonsware.android.recyclerview.videolist;

import android.content.ContentUris;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;

class Video implements Comparable<Video> {
  final String title;
  final Uri videoUri;
  final String mimeType;

  Video(Cursor row) {
    this.title=
      row.getString(row.getColumnIndex(MediaStore.Video.Media.TITLE));
    this.videoUri=ContentUris.withAppendedId(
      MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
      row.getInt(row.getColumnIndex(MediaStore.Video.Media._ID)));
    this.mimeType=
      row.getString(row.getColumnIndex(MediaStore.Video.Media.MIME_TYPE));
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof Video)) {
      return(false);
    }

    return(videoUri.equals(((Video)obj).videoUri));
  }

  @Override
  public int hashCode() {
    return(videoUri.hashCode());
  }

  @Override
  public int compareTo(Video video) {
    return(title.compareTo(video.title));
  }
}

(from Java8/VideoLambda/app/src/main/java/com/commonsware/android/recyclerview/videolist/Video.java)

You want to sort those videos by a custom algorithm: ascending by title, descending by title, etc.

That is easy enough with Collections.sort():


Collections.sort(temp, new Comparator<Video>() {
  @Override
  public int compare(Video one, Video two) {
    return(one.compareTo(two));
  }
});

However, that is fairly verbose. Many languages offer a simpler syntax, where you can provide a “block” or “closure” that, in this case, would handle the comparison without having to create a subclass and implement a method.

Java 8 offers such a syntax, via lambda expressions:

        Collections.sort(newVideos,
          (one, two) -> one.compareTo(two));

(from Java8/VideoLambda/app/src/main/java/com/commonsware/android/recyclerview/videolist/MainActivity.java)

Here, we have replaced the Comparator anonymous inner class implementation with a single line of Java code, but having the same result. The compiler, under the covers, wraps your lambda expression in a suitable Comparator for use by the sort() method.

You Don’t (Yet) Know Jack

Java 8 supports lambda expressions. However, the dx cross-compiler that the Android build tools use to convert javac output into Dalvik bytecodes cannot handle lambda expressions.

But Jack can.

Jack (the Java Android Compiler Kit) is a compiler that converts Java source code directly into Dalvik bytecode. This bypasses the traditional javac compiler from the JDK and the dx cross-compiler. And, along the way, Jack not only supports lambda expressions for Android 7.0, but also generates lambda expression code that works all the way back to API Level 9.

So, to use lambda expressions in our Android app, the official direction is to employ Jack.

Unfortunately, Jack breaks things:

And there may be more. As such, even though Jack is not officially a beta — Google considers it production-ready — you will want to test your code thoroughly before and after switching to Jack.

Using Lambda Expressions

With all that as prologue, let’s look at how we can enable and employ lambda expressions in an Android application.

The code listings for this section come mostly from the Java8/VideoLambda sample project. This is a version of a sample app from the chapter on RecyclerView, where we query the MediaStore for available videos and show them in a list. There are a couple of editions of that sample in the RecyclerView chapter, one of which shows the use of DiffUtil, and that sample also demonstrates lambda expressions.

Enabling Jack

First, you really want to have Android Studio 2.1 or later to be using Jack. You also should be using corresponding versions of the Android Plugin for Gradle and Gradle itself. For example, with Android Studio 2.2, you would use version 2.2.x of the Android Plugin for Gradle (e.g., 2.2.2) and Gradle version 2.14.1. Android Studio 2.2 in particular will complain if you are using older versions of Gradle or the plugin.

Then, you need to add two things to your module’s build.gradle file:

apply plugin: 'com.android.application'

dependencies {
    compile 'com.android.support:recyclerview-v7:25.0.0'
    compile 'com.squareup.picasso:picasso:2.5.2'
}

android {
    compileSdkVersion 25
    buildToolsVersion "25.0.0"

    defaultConfig {
        minSdkVersion 16
        targetSdkVersion 25
        applicationId 'com.commonsware.android.lambda.video'

        jackOptions {
            enabled true
        }
    }

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

(from Java8/VideoLambda/app/build.gradle)

Pondering the Gradle Heap

It is reasonably likely that when you start building with Jack, you will get a lot of messages in the Gradle console like:

``` A larger heap for the Gradle daemon is recommended for running jack.

It currently has 1024 MB. For faster builds, increase the maximum heap size for the Gradle daemon to at least 1536 MB. To do this set org.gradle.jvmargs=-Xmx1536M in the project gradle.properties. For more information see https://docs.gradle.org/current/userguide/build_environment.html ```

To get rid of the error message, follow the instructions, and add a gradle.properties file to the project root directory, with an org.gradle.jvmargs=-Xmx1536M line. If you already have a gradle.properties file for other reasons, just add org.gradle.jvmargs=-Xmx1536M as a new property.

Replacing Listeners with Lambdas

Now, you can replace single-method anonymous inner class implementations with lambdas with relative ease.

The example lambda expression from earlier in this chapter comes from the sortAndApply() method of the VideoAdapter inside the MainActivity of the sample app. Given an ArrayList of Video objects, we need to sort them based on the user’s requested sort order (ascending by default). We use lambda expressions, rather than custom Comparator anonymous inner classes, to do that sorting:

    private void sortAndApply(ArrayList<Video> newVideos) {
      if (sortAscending) {
        Collections.sort(newVideos,
          (one, two) -> one.compareTo(two));
      }
      else {
        Collections.sort(newVideos,
          (one, two) -> two.compareTo(one));
      }

      DiffUtil.Callback cb=new SimpleCallback<>(videos, newVideos);
      DiffUtil.DiffResult result=DiffUtil.calculateDiff(cb, true);

      videos=newVideos;
      result.dispatchUpdatesTo(this);
    }

(from Java8/VideoLambda/app/src/main/java/com/commonsware/android/recyclerview/videolist/MainActivity.java)

Here, each of our two lambda expressions:

Other editions of this sample app have RowController implement the OnClickListener interface and implement onClick() to be able to respond to click events on our rows. This sample app uses a lambda expression instead:

package com.commonsware.android.recyclerview.videolist;

import android.content.ContentUris;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.squareup.picasso.Picasso;

class RowController extends RecyclerView.ViewHolder {
  private TextView title=null;
  private ImageView thumbnail=null;
  private Uri videoUri=null;
  private String videoMimeType=null;

  RowController(View row) {
    super(row);

    title=(TextView)row.findViewById(android.R.id.text1);
    thumbnail=(ImageView)row.findViewById(R.id.thumbnail);

    row.setOnClickListener(view -> {
      Intent i=new Intent(Intent.ACTION_VIEW);

      i.setDataAndType(videoUri, videoMimeType);
      title.getContext().startActivity(i);
    });
  }

  void bindModel(Video video) {
    title.setText(video.title);

    videoUri=video.videoUri;
    videoMimeType=video.mimeType;

    Picasso.with(thumbnail.getContext())
      .load(videoUri.toString())
      .fit().centerCrop()
      .placeholder(R.drawable.ic_media_video_poster)
      .into(thumbnail);
  }
}

(from Java8/VideoLambda/app/src/main/java/com/commonsware/android/recyclerview/videolist/RowController.java)

Here, our lambda expression:

When Android Studio detects a place where you could use a lambda expression, it will mark the anonymous inner class as gray:

Potential Lambda Expression, in Android Studio
Figure 956: Potential Lambda Expression, in Android Studio

A quick-fix (e.g., Alt-Enter on Windows and Linux) will offer to convert it into a lambda expression for you.

Alternative: Method References

Our Video model class has a compareTo() implementation that our sorting lambda expressions rely upon:

package com.commonsware.android.recyclerview.videolist;

import android.content.ContentUris;
import android.database.Cursor;
import android.net.Uri;
import android.provider.MediaStore;

class Video implements Comparable<Video> {
  final String title;
  final Uri videoUri;
  final String mimeType;

  Video(Cursor row) {
    this.title=
      row.getString(row.getColumnIndex(MediaStore.Video.Media.TITLE));
    this.videoUri=ContentUris.withAppendedId(
      MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
      row.getInt(row.getColumnIndex(MediaStore.Video.Media._ID)));
    this.mimeType=
      row.getString(row.getColumnIndex(MediaStore.Video.Media.MIME_TYPE));
  }

  @Override
  public boolean equals(Object obj) {
    if (!(obj instanceof Video)) {
      return(false);
    }

    return(videoUri.equals(((Video)obj).videoUri));
  }

  @Override
  public int hashCode() {
    return(videoUri.hashCode());
  }

  @Override
  public int compareTo(Video video) {
    return(title.compareTo(video.title));
  }
}

(from Java8/VideoLambda/app/src/main/java/com/commonsware/android/recyclerview/videolist/Video.java)

We can further simplify the sorting code by replacing the lambda expressions with a method reference… for the first such expression:


if (sortAscending) {
  Collections.sort(temp, Video::compareTo);
}
else {
  Collections.sort(temp,
    (one, two) -> two.compareTo(one));
}

Here, we are saying that we want to pass the parameters normally passed into compare() of a Comparator into the compareTo() method of the first parameter. You can similarly use method references to refer to static methods or a method on some separate object.

However, we cannot replace the second lambda expression with a method reference. The Video::compareTo method reference always calls the method on the first parameter. In our case, that is fine for the ascending sort, but for the descending sort, we would need to call compareTo() on the second parameter, as our lambda expression does. Alternatively, Video could implement another method (e.g., compareDescendingTo()) that we could use as a method reference.

Jack handles converting method references, as well as lambdas, into code that Android devices back to API Level 9 can handle.