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.
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.
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));
}
}
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));
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.
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:
.class
files from javac
will fail to work, as Jack does not create
those filesAnd 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.
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.
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:
enabled
property of jackOptions
to true
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
}
}
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.
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);
}
Here, each of our two lambda expressions:
compare()
of a Comparator
),
so they get wrapped in parentheses->
)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);
}
}
Here, our lambda expression:
{
and }
)When Android Studio detects a place where you could use a lambda expression, it will mark the anonymous inner class as gray:
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.
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));
}
}
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.