ほとんどのAndroidアプリで書くことになる非同期処理。
非同期処理を順番に実行したい場合や並列に実行させたい事も当然あるわけだけど、これが普通に書くと結構読みにくい。
Androidアプリを作ってる人は同じような悩みを持ってるだろうと色々探してたらいい感じのやつ見つけた。
- jdeferred 【GitHub】
似たようなやつだとこういうのもあるみたいだけど個人的にはjdeferredが好き
- android-promise【GitHub】
こういう感じだといいなーって思ってた要素
- 非同期処理をブロック化してコード上に書きたい
- 例) 非同期処理→3つの非同期を待ち合わせ→2つの非同期処理を待ち合わせ→非同期処理
- こんな複雑な非同期処理しないと思うけど、こういう単位で書けるようにしたい
- 一つ目の非同期が終わった後にその結果を使って二つ目の非同期を実行する処理を簡潔に書ける
- 複数の非同期処理を待ち合わせ処理を簡潔に書ける
- Androidに依存してないもの
- Futureなどからキャンセル処理を行える(非同期処理の制御が行える)
特徴
- JavaでもAndroidでも使える
- 導入が簡単
- 非同期処理をメソッドチェーンで書ける
- 複数の非同期処理を一つにまとめられるので、"この処理はどの非同期処理を組み合わせて最終的にどんな事をしたいのか"といった流れが読みやすくなる
導入
ここではMaven向けで書いてるけどGradleでも使えるよ!
Java・Android共通
<dependency>
<groupId>org.jdeferred</groupId>
<artifactId>jdeferred-core</artifactId>
<version>1.2.1</version>
</dependency>
Androidの場合は追加でどちらかをプロジェクトに合わせて入れる
apklib
<dependency>
<groupId>org.jdeferred</groupId>
<artifactId>jdeferred-android</artifactId>
<version>1.2.1</version>
<type>apklib</type>
</dependency>
aar
<dependency>
<groupId>org.jdeferred</groupId>
<artifactId>jdeferred-android-aar</artifactId>
<version>1.2.1</version>
<type>aar</type>
</dependency>
使い方
// Androidプロジェクトで使う場合はAndroidDeferredManagerを使う
// DefaultDeferredManagerを使うとDoneCallback内がUIスレッドで呼ばれないので落ちるよ
new AndroidDeferredManager().when(new DeferredAsyncTask<Void, Progress, Result>() {
@Override
protected Result doInBackgroundSafe(final Void... params) throws Exception {
return // 処理する内容
}
}).done(new DoneCallback<Result>() {
@Override
public void onDone(final Result result) {
// UIスレッドで実行される
System.out.println(result);
}
}).fail(new FailCallback<Throwable>() {
@Override
public void onFail(final Throwable tr) {
// 例外が投げられると実行される
System.out.println(tr.getMessage());
}
}).always(new AlwaysCallback<Result, Throwable>() {
@Override
public void onAlways(final Promise.State state, final Result resolved, final Throwable rejected) {
// 成功・失敗何かしら返ってきた時に実行される
}
});
このライブラリが便利なとこは成功した時の処理と失敗した時の処理を分けて書けるとこ。
AsyncTaskを使った処理だと分けて書くの少しめんどいのでこのライブラリ使う方が綺麗に書ける。
因みにFail時の引数の型はThrowable固定というわけではなく、別のDeferredクラスを使えば指定できたりする。
非同期処理を繋げたい場合
- 一つめの非同期を実行
- その結果を使って二つ目の非同期を実行
- さらに三つめの非同期を実行
whenメソッドに対してthenでメソッドを繋げていくと処理が連鎖していく
ExecutorService executorService = Executors.newSingleThreadExecutor();
new AndroidDeferredManager(executorService).when(new DeferredAsyncTask<Void, Progress, Result>() {
@Override
protected Result doInBackgroundSafe(final Void... params) throws Exception {
return // 一つ目で処理する内容
}
}).then(new DonePipe<Result, Result2, Failed, Progress>() {
@Override
public Promise<Result2, Throwable, Progress> pipeDone(final Result result) {
return // 一つ目の結果を用いて二つ目の処理を実行
}
}).then(new DonePipe<Result2, Result3, Failed, Progress>() {
@Override
public Promise<Result3, Throwable, Progress> pipeDone(final Result2 result2) {
return // 一つ目の結果を用いて三つ目の処理を実行
}
}).done(new DoneCallback<Result3>() {
@Override
public void onDone(final Result3 result3) {
System.out.println(result3); // 三つ目の処理結果を出力する
}
});
別メソッドに切りださなくてもAsyncTaskのようにネストがどんどん深くならないのが良い。
複数の非同期処理を待ち合わせさせたい場合
こんな感じでメンバ変数に完了した値を覚えさせる処理は必要なし
導入前
public MyActivity extends Activity {
// これいらない
private int finished = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
for(int i = 0; i < 10; i++) {
new AsyncTask<Void, Progress, Result>() {
@Override
protected Result doInBackground(final Void... params) {
return // 処理
}
@Override
protected void onPostExecute(final Result result) {
finished++;
// 全部終わった時
if(finished == 10) {
// 処理
}
}
}.execute();
}
}
}
こんな感じに書ける
DeferredAsyncTask<Void, Progress, Result>[] tasks = new DeferredAsyncTask[10];
for(int i = 0; i < 10; i++) {
tasks[i] = new DeferredAsyncTask<Void, Progress, Result>() {
@Override
protected Result doInBackgroundSafe(final Void... params) throws Exception {
return // 処理
}
};
}
new AndroidDeferredManager().when(tasks).done(new DoneCallback<MultipleResults>() {
@Override
public void onDone(final MultipleResults results) {
System.out.println(results.get(0));
}
}).fail(new FailCallback<OneReject>() {
@Override
public void onFail(final OneReject reject) {
// 何番目の失敗か, 失敗した時に渡されたオブジェクト(Throwableなど)
System.out.println(reject.getIndex() + ", " + getReject());
}
});
配列で渡せば後は全部終わった時どうするかと失敗した時どうするかを書くだけ!
今回紹介した機能中では個人的に一番嬉しかった機能
もちろんwhenに配列で渡した時もPromiseで返ってくるので、doneの代わりにthenで繋げて別の非同期処理に繋げることも可能!
new AndroidDeferredManager().when(tasks).then(new DonePipe<MultipleResults, Result, Failed, Progress>() {
@Override
public Promise<Result, Failed, Progress> pipeDone(final MultipleResults result) {
return // 別の非同期処理
}
}).done(...).fail(...);
Futureを用いたDeferredの使い方
Androidに依存しない処理でも非同期処理を書きたい場合もあるけど、そういう時はFutureを使って記述する事ができる。
Futureに限らずCallableなど他にも使えるけど、ここでFutureを使うのはActivityの終了に合わせて非同期処理をキャンセルさせたいため。
DeferredFutureTask<Result, Progress> future = new DeferredFutureTask<Result, Progress>(new Callable<Result>() {
@Override
public Result call() throws Exception {
return // 処理
}
}) {
@Override
public boolean cancel(final boolean mayInterruptIfRunning) {
// 処理のキャンセル(httpリクエストならdisconnectなど)
return super.cancel(mayInterruptIfRunning);
}
};
AsyncTask.SERIAL_EXECUTOR.execute(future);
new AndroidDeferredManager().when(future).done(...).fail(...);
// Activity#onDestroyなど呼ばれた時の処理
future.cancel(true);
Futureを使うことでホームボタンで戻っても裏で通信が走り続けないようにできる(他のやり方でもできるのかな?)
ラムダ式を使う
Retrolambdaを使うともっと簡潔に書くことができる。
実際に使うのであれば、Retrolambdaを用いて書いた方が可読性高く表現できるようになる。
ExecutorService executorService = Executors.newSingleThreadExecutor();
new AndroidDeferredManager(executorService).when(() -> return /* 一つ目で処理する内容 */)
.then(result -> return /* 一つ目の結果を用いて二つ目の処理を実行 */)
.then(result2 -> return /* 二つ目の結果を用いて三つ目の処理を実行 */)
.done(result3 -> System.out.println(result3));/*三つ目の処理結果を出力する */
感想
今回の記事はAndroid向けだったけど、Javaプロジェクトでも使えるので積極的に使っていきたい。
注意点として、AndroidDeferredObjectの生成場所をバックグラウンドでやるとDoneCallbackの中身がAndroidDeferredObjectを生成したスレッドで実行されてしまって落ちる可能性があるので、対応されるまで気をつけた方が良いと思う。
因みに紹介した通りに使っている分にはまずこの問題は起きないので大丈夫。
この問題はこのissueで解決されるはず。
https://github.com/jdeferred/jdeferred/pull/32
非同期処理の簡潔化の選択肢は他だとAndroidAnnotationsを使った方法もあるので、良いと思う方を試してみるといいかも。
このライブラリはjQueryのDeferredとPromiseを参考にしたライブラリなので、これ使って設計する時はそこらへん把握してから使う方が分かりやすい。
DeferredとPromise使って何が嬉しいのかについてはここらへんが読みやすいと思う。
http://qiita.com/yuku_t/items/1b8ce6bba133a7eaeb23
追記(2016-04-06)
現在はRxJavaに移行しました。個人的にはRxJavaの方が好みかな。
たまにストック数が増えるので、現在の好みを追記しておこうかなと。
とは言えjdefferedが非推奨というわけでもなく、RxJavaよりとっつきやすいとは思うので、
チーム方針だったりそこらへん相談しながら入れるといいんじゃないかなーと思います。