techium

このブログは何かに追われないと頑張れない人たちが週一更新をノルマに技術情報を発信するブログです。もし何か調査して欲しい内容がありましたら、@kobashinG or @muchiki0226 までいただけますと気が向いたら調査するかもしれません。

AndroidのPercelデータを自動生成するAutoValueを使ってみる

Googleがautoというライブラリ群の中にあるAutoValueというデータクラスを自動生成するライブラリがあります。
このAutoValueではJavaの一般的なデータクラスの生成しかできないためAndroidのインターフェースのParcelableを実装したものを自動生成することができません。

そのAutoValueを拡張してParcelableの実装も自動生成してくれるライブラリauto-value-parcelをご紹介します。

準備

まず準備としてAutoValueはAnnotation Processorの機能を利用してクラスを自動生成します。
そのためaptの設定とライブラリなどの設定をbuild.gradleに行う必要があります。
android-apt:をまずはbuild.gradleに記載します。

[build.gradle]

buildscript {

<省略>

    dependencies {
        classpath 'com.android.tools.build:gradle:2.2.0-rc1'
        classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
    }
}

<省略>

さらにaptを使えるようにapplyでandroid-aptを指定し、googleのauto-valueとauto-value-parcelをdependenciesに記載します。

[app/build.gradle]

apply plugin: 'com.android.application'
apply plugin: 'android-apt'

<省略>

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile 'com.google.auto.value:auto-value:1.0-rc1'
    apt 'com.ryanharter.auto.value:auto-value-parcel:0.2.4-rc2'
    compile 'com.android.support:appcompat-v7:24.2.0'
}

これで準備は完了です。

データクラスの作成

AutoValueはabstractクラスにアノテーションAutoValueをつけたものがParcelableを持つクラスを自動生成してくれます。
次のようなクラスがあったとします。

[Todo.java]

@AutoValue
public abstract class Todo implements Parcelable {

    public abstract String name();

    public abstract TodoDetail detail();

}

内部にはTodoDetailという独自クラスを持っているとし、TodoDetailは下記の定義があったとします。

[TodoDetail.java]

@AutoValue
public abstract class TodoDetail implements Parcelable {

    public abstract String detail();

}

このケースはデータクラス内に更にParcelableのクラスを持っている状態で自動生成してみました。

自動生成後のクラス

自動生成されたクラスは次のようになります。

[AutoValue_Todo]

final class AutoValue_Todo extends $AutoValue_Todo {
  public static final Parcelable.Creator<AutoValue_Todo> CREATOR = new Parcelable.Creator<AutoValue_Todo>() {
    @Override
    public AutoValue_Todo createFromParcel(Parcel in) {
      return new AutoValue_Todo(
          in.readString(),
          (TodoDetail) in.readParcelable(TodoDetail.class.getClassLoader())
      );
    }
    @Override
    public AutoValue_Todo[] newArray(int size) {
      return new AutoValue_Todo[size];
    }
  };

  AutoValue_Todo(String name, TodoDetail detail) {
    super(name, detail);
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    dest.writeString(name());
    dest.writeParcelable(detail(), flags);
  }

  @Override
  public int describeContents() {
    return 0;
  }
}

[$AutoValue_Todo]

 abstract class $AutoValue_Todo extends Todo {

  private final String name;
  private final TodoDetail detail;

  $AutoValue_Todo(
      String name,
      TodoDetail detail) {
    if (name == null) {
      throw new NullPointerException("Null name");
    }
    this.name = name;
    if (detail == null) {
      throw new NullPointerException("Null detail");
    }
    this.detail = detail;
  }

  @Override
  public String name() {
    return name;
  }

  @Override
  public TodoDetail detail() {
    return detail;
  }

  @Override
  public String toString() {
    return "Todo{"
        + "name=" + name + ", "
        + "detail=" + detail
        + "}";
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (o instanceof Todo) {
      Todo that = (Todo) o;
      return (this.name.equals(that.name()))
           && (this.detail.equals(that.detail()));
    }
    return false;
  }

  @Override
  public int hashCode() {
    int h = 1;
    h *= 1000003;
    h ^= this.name.hashCode();
    h *= 1000003;
    h ^= this.detail.hashCode();
    return h;
  }

}

[AutoValue_TodoDetail]

final class AutoValue_TodoDetail extends $AutoValue_TodoDetail {
  public static final Parcelable.Creator<AutoValue_TodoDetail> CREATOR = new Parcelable.Creator<AutoValue_TodoDetail>() {
    @Override
    public AutoValue_TodoDetail createFromParcel(Parcel in) {
      return new AutoValue_TodoDetail(
          in.readString()
      );
    }
    @Override
    public AutoValue_TodoDetail[] newArray(int size) {
      return new AutoValue_TodoDetail[size];
    }
  };

  AutoValue_TodoDetail(String detail) {
    super(detail);
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    dest.writeString(detail());
  }

  @Override
  public int describeContents() {
    return 0;
  }
}

[$AutoValue_TodoDetail]

 abstract class $AutoValue_TodoDetail extends TodoDetail {

  private final String detail;

  $AutoValue_TodoDetail(
      String detail) {
    if (detail == null) {
      throw new NullPointerException("Null detail");
    }
    this.detail = detail;
  }

  @Override
  public String detail() {
    return detail;
  }

  @Override
  public String toString() {
    return "TodoDetail{"
        + "detail=" + detail
        + "}";
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (o instanceof TodoDetail) {
      TodoDetail that = (TodoDetail) o;
      return (this.detail.equals(that.detail()));
    }
    return false;
  }

  @Override
  public int hashCode() {
    int h = 1;
    h *= 1000003;
    h ^= this.detail.hashCode();
    return h;
  }

}

Parcel化してもとのデータに戻る処理が自動的に生成されているのがわかります。
また、きちんとメンバにデータクラスを持っているときでもきちんと生成されているため問題なく利用できることが確認できます。

使うときは次のようになります。

        TodoDetail detail = new AutoValue_TodoDetail("詳細");
        Todo todo = new AutoValue_Todo("TODO", detail);

        Log.d("Techium", todo.name());
        Log.d("Techium", todo.detail().detail());

        Bundle bundle = new Bundle();
        bundle.putParcelable("Techium", todo);
        Todo output = bundle.getParcelable("Techium");

        Log.d("Techium", output.name());
        Log.d("Techium", output.detail().detail());

目的のデータがParcel化してBundleにデータを格納することができます。

所感

Parcelalbeを持つデータクラスの生成はparceler-apiなどもあります。
parceler-apiは実際はParcelableのデータ自体を持っているわけではなくそれを変換したデータクラスを生成するwrapとそれを元のデータクラスに戻すunwrapをデータが送る側と受け取る側が意識しないと行けない問題がありますので利用場面を考えて使う必要があります。
AutoValueParcelの場合はParcelableのクラス自体を自動生成するために特に意識することがなく受け取り側の実装を一般的なParcelの実装と異なったことはしないで実装可能です。
ただし、AutoValueParcelにも欠点があります。
AutoValueParcelはデータの設定がコンストラクタ時にしかできないためあとからのデータ変更に弱いという問題があり、どちらでも利用場面に応じて選択する必要があります。