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.

Also, you should now be using Android Studio 3.0, and its related build tools.

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.

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 Lambda Expressions

Prior to Android Studio 3.0 (and its associated build tools), lambda expression support was offered by Google through a different Java compiler, called Jack. Jack had issues, possibly due to a broken crown. Android Studio 3.0 eliminated Jack, offering lambda expression support by means of the classic javac compiler combined with an updated version of the dx tool for creating Dalvik bytecode from Java bytecode.

So, nowadays, to use lambda expressions, so long as you are using current build tools, the only project change that you need is to indicate that your source and target are both Java 1.8:

apply plugin: 'com.android.application'

dependencies {
    implementation 'com.android.support:recyclerview-v7:27.1.1'
    implementation 'com.android.support:support-fragment:27.1.1'
    implementation 'com.squareup.picasso:picasso:2.5.2'
}

android {
    compileSdkVersion 27

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

    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}
(from Java8/VideoLambda/app/build.gradle)

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=row.findViewById(android.R.id.text1);
    thumbnail=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 358: 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.