Realm + Retrolambda = Awesome

  • 34
    Like
  • 0
    Comment
More than 1 year has passed since last update.

(この記事は OS X Yosemite 10.10.2 が動作する Mac mini の上で書かれました)

TL;DR

Realm 0.77 から使えるようになった executeTransaction を Retrolambda 化してあげると最高。

realm.executeTransaction(_realm -> {
  // Awesome
});

はじめに

Realm Tech Talk #1, #2Realm Meetup の開催もあり、日本で Realm が盛り上がり始めました。 Android 版に関しては iOS 版よりも少し機能が遅れていましたが、最近、様々な機能が追加されました。今回は Realm と Retrolambda を組み合わせて便利に使ってみようと思います。

Realm について

Realm (レルム) は Core Data や SQLite を置き換える目的で作られているモバイル向けのデータベースソフトです。速く、省メモリで、使いやすい API を提供しています。現在は iOS 版と Android 版があり、 Apache License, Version 2.0 で OSS として公開されています。

DB のコアレイヤーは現在 Tightdb という名前の C++ で書かれたバイナリが同梱されている状況で、こちらは OSS になっていませんが、Realm Tech Talk における JP Simard さんの発表によれば、今後 OSS 化されるそうです (ちなみに Tightdb は Realm と名前を変える前の会社名だそう) 。

Retrolambda について

Retrolambda (レトロラムダ) は Java 8 で提供されるようになったラムダ式を Java 5, 6, 7 で使えるようにするツールです。 Android では Android 4.4 Kitkat 以上だけを見て初めて Java 7 に対応できるという状況で、現実的にはダイヤモンドオペレータや Switch 文における文字列を使った分岐などの一部機能を除けば、ほとんど Java 6 にしか対応していません。

しかし、Retrolambda を使えば少なくともラムダ式は取り入れることができるようになります。 Android は匿名クラスを多用するので、ボイラープレートが増えてインデントが深くなるという辛さがありました。ラムダ式と SAM 変換を使えば、スッキリと書くことができます。

Thread の例

// これまでの Thread
new Thread(new Runnable() {
  @Override
  public void run() {
    // ここで初めてやりたい仕事が書ける
  }
}).start();

// Retrolambda を使った Thread
new Thread(() -> {
  // すぐにやりたい仕事を書き始められる
}).start();

View#setOnClickListener の例

// これまでの setOnClickListener
findViewById(R.id.button).setOnClickListener(new View.setOnClickListener() {
  @Override
  public void onClick(View view) {
    // クリックしたときの処理
  }
});

// Retrolambda を使った setOnClickListener
findViewById(R.id.button).setOnClickListener(view -> {
  // クリックしたときの処理
});

Retrolambda の導入に関しては、以下の記事を参考にしました。

Realm 復習

2015/02/22 現在で Realm の Android 版である realm-java は 0.79.1 が最新版です。 Realm は 0.01 のアップデートでも大幅な強化や、後方互換性の無い修正が入ることがあるので、少なくとも 1.0 が登場するまではバージョンアップに注視する必要があります。

Realm をプロジェクトに組み込む

もはや Android Studio を使わない理由は無いので、アプリケーションプロジェクトの build.gradle の dependencies に

compile 'io.realm:realm-android:0.79.1'

を追加して同期しましょう。

Realm で管理するクラスを定義する

Realm で管理するクラスは RealmObject を継承する必要があります。そしてかなりの制約があります。

  1. フィールドは private でなければならない
  2. 定義したフィールドには Setter / Getter を用意しなければならない
  3. Setter / Getter にはフィールドに値をセットする、取り出す以外のロジックは書けない
  4. RealmObject を継承したクラスは Setter / Getter 以外のメソッドを置けない

といったものです。コードにしてみると、

public class Book extends RealmObject {
  @PrimaryKey
  private long bookId;
  @Index
  private String title;
  @Index
  private String author;
  private int publishYear;

  public void setBookId(long bookId) { this.bookId = bookId; }
  public long getBookId() { return this.bookId; }
  public void setTitle(String title) { this.title = title; }
  public String getTitle() { return this.title; }
  public void setAuthor(String author) { this.author = author; }
  public String getAuthor() { return this.author; }
  public void setPublishYear(int publishYear) { this.publishYear = publishYear; }
  public int getPublishYear() { return this.publishYear; }
}

こんな感じです。特に厳しいのが Setter / Getter 以外のメソッドを置けないところでしょうか。 equalshashCode はもちろん、toString も置けないので、ログに出すときにダンプするには別のレイヤーにロジックを置いてあげる必要があります。

また、0.79 から @PrimaryKey アノテーションが使えるようになりました。 Android 版にはしばらく無かった機能で、心待ちにしていたものの1つです。後述のオブジェクト作成と保存の際に役立ちます。

オブジェクトを作成して保存する

0.77 からオブジェクトを単品で作って、後から Realm に保存することができるようになりました。また、 executeTransaction メソッドによってトランザクションの発行が簡単になりました。

// メモリにオブジェクトを生成
final Book book = new Book();
book.setBookId(1);
book.setTitle("Realm cookbook");
book.setAuthor("Realm fanboy");
book.setPublishYear(2005);

// トランザクションを発行して保存
realm.executeTransaction(new Realm.Transaction() {
  @Override
  public void execute(Realm realm) {
    realm.copyToRealmOrUpdate(book);
  }
});

これによって GSON や Retrofit を使って Realm で管理するオブジェクトを生成できるようになりました。ただし、そのままではスタックオーバーフローが発生してクラッシュしてしまうので、ドキュメントの Other libraries を参考に、 GsonBuilder で独自の Gson オブジェクトを生成する必要があります。

final Gson gson = new GsonBuilder().
  setExclusionStrategies(new ExclusionStrategy() {
    @Override
    public boolean shouldSkipField(FieldAttributes f) {
      return f.getDeclaringClass().equals(RealmObject.class);
    }

    @Override
    public boolean shouldSkipClass(Class<?> clazz) {
      return false;
    }
  }).create();

トランザクションの中で使っている copyToRealmOrUpdate メソッドは 0.79 で追加されたメソッドの1つです。名前の通りで、 Realm にオブジェクトを保存するか更新します。更新の際は @PrimaryKey アノテーションが付いているフィールドの値を見て、既に存在していたら更新します。

copyToRealmUpdate メソッドは @PrimaryKey が付いていない RealmObject の保存はできないので要注意です。保存しようとすると例外が発生します。その場合は単純な copyToRealm メソッドを使って保存しましょう。

Retrolambda と組み合わせる

ここまで書いていてお気づきの方もいらっしゃると思いますが、

realm.executeTransaction(new Realm.Transaction() {
  @Override
  public void execute(Realm realm) {
    realm.copyToRealmOrUpdate(book);
  }
});

は Retrolambda と相性が良さそうですね? 早速置き換えてみましょう。

realm.executeTransaction(_realm -> {
  _realm.copyToRealmOrUpdate(book);
});

非常にスッキリしました。匿名クラス内に書かれた変数名 realm_realm としている点に注意してください。同じ名前は使えません。もしもこの処理を、executeTransaction 無しで書いたとしたら、

realm.beginTransaction();
try {
  realm.copyToRealmOrUpdate(book);
  realm.commitTransaction();
} catch(Exception e) {
  realm.cancelTransaction();
}

という長ったらしいコードになります。素直に executeTransaction を使った場合は行数的にはそこまで変わらないですが、 Retrolambda と組み合わせることで、非常に見通しの良い形になりました。最高です。