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:

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:

This will be added in a future update to Room.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.