Immutability via AutoValue
Many developers who elect to make immutable classes in Java elect to use Google’s AutoValue library. This library uses annotations and code generation to help enforce immutability, while also handling aggravating details like implementing equals()
, hashCode()
, and so forth.
For basic stuff, using AutoValue is fairly simple: implement an abstract
class with abstract
getter methods for the data that you want the immutable class to hold. Add the @AutoValue
annotation — along with the dependency that supplies it — and AutoValue takes over from there.
Earlier in the book, we had the Sensor/LiveList
sample app, where we wrapped the SensorManager
in a LiveData
. The Sensor/AutoSensor
sample project is a clone of that one, where we use AutoValue for the event objects.
The original project had a simple Event
static class inside of SensorLiveData
, using final
for its limited immutability:
static class Event {
final Date date=new Date();
final float[] values;
Event(SensorEvent event) {
values=new float[event.values.length];
System.arraycopy(event.values, 0, values, 0, event.values.length);
}
}
The revised project pulls that Event
class out to a top-level AutoSensorEvent
class and applies AutoValue to it:
package com.commonsware.android.livedata;
import android.hardware.SensorEvent;
import com.google.auto.value.AutoValue;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
@AutoValue
abstract class AutoSensorEvent {
abstract long date();
abstract List<Float> values();
static AutoSensorEvent from(SensorEvent event) {
ArrayList<Float> values=new ArrayList<>();
for (float value : event.values) {
values.add(value);
}
return(new AutoValue_AutoSensorEvent(System.currentTimeMillis(),
Collections.unmodifiableList(values)));
}
}
Annotating an abstract
class with @AutoValue
causes AutoValue to find all getter-style abstract
methods — in this case, date()
and values()
. AutoValue then code-generates a shadow class, AutoValue_AutoSensorEvent
, that is a concrete implementation of the AutoSensorEvent
API. We use the concrete class constructor to make instances of an AutoSensorEvent
-compatible class. Outside parties using AutoSensorEvent
should neither know nor care that the actual implementation is actually AutoValue_AutoSensorEvent
. The AutoValue_AutoSensorEvent
class not only handles our two data values but also the equals()
, hashCode()
, and toString()
methods as well.
Our from()
factory method sets up the data to be passed to the AutoValue_AutoSensorEvent
constructor. We use unmodifiableList()
to ensure that nobody can modify the contents of the values()
List
, and since Float
itself is immutable, that makes values()
immutable “all the way down”. Similarly, the long
that is returned by date()
is immutable, so nothing can be changed in the AutoSensorEvent
.
All of this is possible because we are adding AutoValue’s dependencies:
dependencies {
implementation 'com.android.support:recyclerview-v7:28.0.0'
implementation 'com.android.support:support-fragment:28.0.0'
implementation 'android.arch.lifecycle:runtime:1.1.1'
implementation 'android.arch.lifecycle:livedata:1.1.1'
compileOnly 'com.google.auto.value:auto-value:1.5.2'
annotationProcessor 'com.google.auto.value:auto-value:1.5.2'
}
Here, the same dependency (com.google.auto.value:auto-value
) is used twice. The annotationProcessor
dependency enables the compile-time handling of @AutoValue
and related annotations. The provided
dependency adds in runtime support code that the generated code depends upon.
AutoValue itself has many more features, including:
- Generating an optional builder-style API for constructing instances of the
@AutoValue
-annotated class - Support for
@Nullable
to indicate if a value can benull
or not - Memoization support for caching the results of derived values, which is particularly useful if those calculations are expensive
AutoValue and LiveData
LiveData
and AutoValue work together nicely. The revised SensorLiveData
simply uses the from()
factory method to create AutoSensorEvent
instances that wrap up the data we want to cache from a SensorEvent
:
package com.commonsware.android.livedata;
import android.arch.lifecycle.LiveData;
import android.content.Context;
import android.hardware.Sensor;
import android.hardware.SensorEvent;
import android.hardware.SensorEventListener;
import android.hardware.SensorManager;
class SensorLiveData extends LiveData<AutoSensorEvent> {
final private SensorManager sensorManager;
private final Sensor sensor;
private final int delay;
SensorLiveData(Context ctxt, int sensorType, int delay) {
sensorManager=
(SensorManager)ctxt.getApplicationContext()
.getSystemService(Context.SENSOR_SERVICE);
this.sensor=sensorManager.getDefaultSensor(sensorType);
this.delay=delay;
if (this.sensor==null) {
throw new IllegalStateException("Cannot obtain the requested sensor");
}
}
@Override
protected void onActive() {
super.onActive();
sensorManager.registerListener(listener, sensor, delay);
}
@Override
protected void onInactive() {
sensorManager.unregisterListener(listener);
super.onInactive();
}
final private SensorEventListener listener=new SensorEventListener() {
@Override
public void onSensorChanged(SensorEvent event) {
setValue(AutoSensorEvent.from(event));
}
@Override
public void onAccuracyChanged(Sensor sensor, int accuracy) {
// unused
}
};
}
AutoValue and Room
Unfortunately, AutoValue and Room 1.x do not work together, at least for @Entity
classes:
- Room wants to work with a constructor, and an
@AutoValue
abstract
class cannot have a constructor - Annotations like
@PrimaryKey
and@ColumnInfo
go on fields, and an@AutoValue
class has no fields - Room’s documentation indicates that it wants setter methods or
public
fields, and an@AutoValue
class has neither
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.