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.
Also, you should now be using Android Studio 3.0, and its related build tools.
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.
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.
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
}
}
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=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);
}
}
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 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.
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.