読者です 読者をやめる 読者になる 読者になる

visible true

技術的なメモを書く

RxAndroidとRetrolambdaで大体Java8をAndroidに持ち込む

※これもう大分古いので AndroidでJava8環境 2016 - visible true も御覧ください。

はじめに

RxAndroid(というかRxJava)とRetrolambdaでそろそろ大体Java8な環境でAndroidアプリケーションが開発出来るのではないかと考えて試してみた。

2014年4月頃のRetrolambdaはまだ1.1.4とかでlambdaをおまけ程度に使える程度で「あー戯れって感じねはいはい」という事でスルーしていたんだけど、最近久しぶりに覗いたら1.8.0まで育っていてTry-with-resources*1Method referencesがサポートされていた。

また、RxJavaは1.0.0が11月にリリースされて実用段階に入ってきており、Java8のStream APIを概ね補完する様な機能を持っている。

更にAndroidでRxJavaを扱うための便利なUtillity集であるRxAndroidも作られ始めた(これはまだ破壊的変更がガンガン入ってるので実用段階かというとちょっと微妙かも。でもAndroidの為に設計されているのでAndroiderにとってはRxJavaの勉強の良いサンプルになるとおもう)。

ていう事で結構いい感じの環境が作れるのではないか。

リポジトリ

試した成果物は以下の通りです。シンプルなTodoアプリです。

f:id:sys1yagi:20150101184618p:plain

設定

リポジトリの方では色々なライブラリを使ってますがここでは割愛します。RxAndroidとRetrolambdaに関する設定は以下の通りです。

buildscript {
  repositories {
    jcenter()
  }
  dependencies {
    classpath 'com.android.tools.build:gradle:1.0.0'
    classpath 'me.tatarka:gradle-retrolambda:2.5.0'
  }
}

apply plugin: 'com.android.application'
apply plugin: 'me.tatarka.retrolambda'

android {
  compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
  }
}

dependencies {
  compile 'io.reactivex:rxandroid:0.23.0'
}

肝はsourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8でしょう。PATHにJava8が通っている必要があります。Java7になっている場合は変更が必要です(retrolambdaの設定でJAVA8_HOME等をセットする事もできるようです)。

javac -version
>javac 1.8.0

Lambda

とりあえずlambdaを。もはや説明はいらない気がする。retrolambdaのおかげでガリガリ使えます。lambdaだけだとただのシンタックスシュガーだねくらいの感じですがRxJavaと組み合わせると大分簡潔に書けて良いです。以下はListViewにOnItemClickListenerをセットする例。

listView.setOnItemClickListener((parent, view, position, id) -> {
  Todo todo = adapter.getItem(position);
  editTodo(todo);
});

Method Reference

lamdaに加えてMethod Referenceもretrolambdaがサポート。これで以下の様なコードを書けるように。この時ActivityではOnItemClickListenerをimplementする必要がない。SAM typeいいっすね。これもRxJavaと組み合わせて色々簡潔に書けるようになります。

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
  //... 
  listView.setOnItemClickListener(this::onItemClick);
}

public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
  //do something
}

try-with-resources

try-with-resourcesについてはretrolambdaでは解決できず。API Level 19以上からサポートしてるので使いたければminSdkVersionを19にすればいけます...。ただOkHttpとかRetrofitとかcommons-ioとかライブラリ使ってると直接try-with-resources使う事ってほとんどない気がする。

Optional

※追記 2014/01/26RxJavaのObservable<T>でOptional<T>を代行する - visible trueでOptionalもいい感じにいけそうだという事がわかりました。

Optionalもretrolambdaではサポートしていない。また良さ気なbackportライブラリも見当たらなかった。Guavaに入っているがちょっとGuavaはでっかいので気軽に導入するのは気が引ける。Optionalは小さいセットなのでOpen JDKからソースを引っ張ってきてもいいかもしれない。

Stream API

lambdaやmethod referenceは単体だと「あー簡潔だ」くらいの印象しかないけど、Stream APIと組み合わせると強力になる。残念ながらStream APIはretrolambdaではサポートしていない。そこで近い機能セットを持つRxJavaと組み合わせて代替する。

Stream APIとRxJavaは本質的にコンセプトが異なるらしい*2けど大体同じ事出来るのでいいんじゃないかなと思う。

幾つか実装例を列挙します。

ArrayAdapter<T>をiteratableにする

ArrayAdapter<T>は内部の要素をprivate List<T> mObjects;で管理していて、取り出すにはT getItem(int)しかない。要素全体に何か処理をしたい場合にgetCount()を使ってfor文を書くことになる。以下の実装を加えるといい感じにiteratableになる。stream()を生やすというのに近いんじゃないだろうか。

public Observable<T> items() {
  return Observable
      .range(0, getCount())
      .map(i -> getItem(i))
      .window(getCount())
      .toBlocking()
      .single();
}

これを生やしておくと、「checkが付いている要素をAdapterから削除する」といった処理を以下の様に簡潔に書ける。集計処理などでも便利に使える。

adapter.items()
  .filter(Todo::isChecked)
  .subscribe(adapter::remove);

参考となる実装は以下*3

EditTextが空だったらButtonをdisabledにする

RxAndroidを使った簡単な例。editTextの中身を監視して空だったらbuttonをdisabledにする。lambdaとmethod referenceでいい感じに。ViewObservableはRxAndroidが提供するクラス。この他AndroidObservableなどがある*4

EditText editText;
Button button;
//...
ViewObservable.text(editText)
    .map(event -> !TextUtils.isEmpty(event.text))
    .subscribe(button::setEnabled);

ディレクトリのファイル一覧を再帰的に取り出す

ちょっと複雑な例。基本的な実装は以下。指定したFileをrootとし、再帰的にディレクトリを辿ってファイル一覧を返す。

public Observable<File> files(File f) {
  if (f == null) {
    return Observable.empty();
  }
  if (f.isDirectory()) {
    return Observable.from(f.listFiles()).flatMap(this::files);
  } else {
    return Observable.just(f);
  }
}

サンプルアプリでは、TodoのデータをContext.getFileDir()で取得出来る場所に保存する様にした。FileManagerというクラスを以下の様に実装しておき、

public class FileManager {
  Context context;
  public File getFileDir() {
    return context.getFilesDir();
  }
  public Observable<File> getFileDirFiles() {
    return files(getFileDir());
  }
  public <T> T loadJsonFromFileDir(String name, Class<T> clazz) {
    //...
  }
  public Observable<File> files(File f) {
    //...
  }
}

保存したファイル一覧を取り出して読み込むといった処理に使ったりできる。また条件に合ったファイルを削除するといった事も簡単にできる。

public Observable<Todo> list() {
  return fileManager
      .getFileDirFiles()
      .filter(file -> file.getName().startsWith(Todo.PREFIX))
      .map(file -> fileManager.loadJsonFromFileDir(file.getName(), Todo.class));
}

まとめ

振り返ると結構Java8の機能足りてないなと思ったけど十分強力だなーと思う。Groovyでも書いてみたけどGroovyの場合lambdaじゃなくてclosureなので時折型が合わない事があったりjavaの世界から触るとき面倒だったりするのでJava8環境を目指した方が楽な印象がある。

  • Retrolambdaでlambda, method referenceをサポートする
  • RxJavaでStream APIの代替をする
  • try-with resourcesはminSdkVersion 19が必要
  • Optionalはない。Guavaを検討する。ただしGuavaはでかい。

Java8サイコー

*1:AndroidではJava7サポートが中途半端なのでRetrolambdaでは使えない

*2:RxJava vs Java 8 Parallelism Stream

*3:window, toBlocking, singleは無くても動作するが、onNext()の中でadapter.remove()すると死ぬのでwindowで一回切り離している

*4:masterの最新では既にこの辺りのクラスは機能別に更に分割されていて今回利用している0.23.0と互換性はない