便利なジョブキューライブラリを公開したので紹介します! :)
はじめに
最近、Androidで重たい処理を実行したいけど、その結果はすぐにUIに反映する訳でもなく、ただいつかは確実に実行して欲しいジョブが出てきます。だいたいのアプリはUIの反応を良くするためや頑丈な設計にするために、こういったジョブを何らかの方法でバックグランドで実行をしていますが、いくつか乗り越えないといけない問題があります、まずはそれらの問題について書いていきます。
問題
こういったバックグラウンド処理を簡単に実現するためにAndroid SDKは幾つかの便利なAPIを提供してくれてます。代表的なものには、AsyncTask、Loaderがありますが、それらはいくつか癖があるので、まずはそこを書いてから。
AsyncTask
AsyncTaskに関しては、ActivityのLifecycleに非常に密接に結合されていて、画面回転でActivityが再生成されるだけでそのAsyncTaskの処理内容を無駄にしてしまいます(返すactivityはもう存在しないから)。特にネットワーク関係の処理をバックグラウンドで行っていると、実行は正しく行われたかどうかも分からない、結果も分かりません。ネットワーク通信が切れている時のRetryを実現するのも非常に面倒です。しかも、Activityが再生成されたところで、AsyncTaskは再生成されずにプログラムからまた呼び出さないといけない、それに対応するために増えた行数分でプログラムのバグを作り出しそうであんまり良くないですね。
Loader
Loaderに関しては、AsyncTaskに比べると少しマシで、上で説明したような画面回転のAcitivyの再生成が行われても大丈夫。Loaderも一緒に再生成されます。しかし、特にネットワーク通信をしているジョブに関しては、一回別のActivityを開いて、また戻ると2重にリクエストを飛ばすことになって、無駄な通信が増えます。モバイル環境ではできるだけ無駄を無くしたいものです。AsyncTask同様、Retryを実現するのは面倒です。
実世界の例
最近作っていたアプリでは、ネットワークの状態が悪くてもユーザが操作できる仕様になっていて、あるUIの操作を行うとサーバにその操作内容を知らせる必要がありました。しかし、その結果は特に気にする必要はなく、ネットワークが復帰した段階でサーバに送るのがこのアプリの仕様では十分でした。(一部は嘘(笑): UIが表示されていればその結果をもらって、画面に反映させるところもある。表示していなければ特に何もしない)
まとめると、こんなことをしてくれる何かが欲しかった。
1) Activityのライフサイクルに関係しないジョブをバックグラウンドで実行
2) ネットワーク接続がある時にジョブが実行されればそれで良い
3) ネットワークがない場合は、復帰した時にキューに積まれたジョブをすべて実行
そこで、同僚・先輩・友人のYuki Fujisakiさんが作っていたコードをベースに、 Nirai というジョブキューを公開してみた!
ライブラリの紹介
Nirai - A job queue for Android
http://github.com/jfsso/nirai
※じつは、「Nirai」は「願い」が由来です! :)
使い方
上記のGithubページに書いてますが、ここは日本語で頑張ります。
設定方法
はじめに、Gradle/Mavenのdependencyを指定することでniraiをインポートすることができます。Eclipse使っている場合に関してはおいおい対応したいとは思っていますが、現在のところはソースコードをダウンロードしてEclipseに何らかの方法でライブラリプロジェクトを生成してからインポートすると使えるでしょう。
Gradle:
dependencies {
compile 'jp.joao:nirai:0.1.0'
}
Maven:
<dependency>
<groupId>jp.joao</groupId>
<artifactId>nirai</artifactId>
<version>0.1.0</version>
</dependency>
次に、マニフェストファイルにNirai関連のServiceとBroadcastReceiverを追加します。このBroadcastReceiverはジョブがない状態の時は動的に無効化されるので、無駄に呼ばれてしまうことはないので、ご心配なく。
nirai.JobServiceに関しては、IntentServiceとなっていて、ジョブを逐次に処理していきます。
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<service android:name="nirai.JobService" android:exported="false" />
<receiver
android:name="nirai.JobServiceTrigger"
android:enabled="false" >
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
ジョブを実行するJobRunnerの定義
非常にシンプルなジョブです。ここで注意が必要なので、実行はUIスレッドでは行われません。サンプルプロジェクトではToastを表示しているため、その部分だけをUIスレッドで実行するようにしています。
import nirai.JobRunner;
import nirai.exception.NetworkOfflineException;
public class SimpleJobRunner implements JobRunner {
@Override
public void run(final Context context, Map<String, Object> args) throws NetworkOfflineException {
ThreadUtils.toastOnUiThread(context.getApplicationContext(), "simple job run! ;)", Toast.LENGTH_SHORT);
}
}
ネットワークの状態が良くない時は、NetworkOfflineExceptionを投げるすると、ネットワークが回復した時にリトライします。ネットワーク状態ではなく、サーバの何らかの異常の場合はバックオフをしていくので、サーバ側が復帰した時の自業自得DoSが避けられます。
import nirai.JobRunner;
import nirai.exception.NetworkOfflineException;
public class NetworkJobRunner implements JobRunner {
@Override
public void run(Context context, Map<String, Object> args) throws NetworkOfflineException {
try {
/** some code that hits the network **/
httpclient.execute(new HttpGet("http://www.google.com/"));
} catch (ClientProtocolException e) {
// can't recover, log and ignore it
ThreadUtils.toastOnUiThread(context.getApplicationContext(), "network job ended with an unrecoverable error", Toast.LENGTH_SHORT);
} catch (IOException e) {
// try again
ThreadUtils.toastOnUiThread(context.getApplicationContext(), "network job enqueued for later", Toast.LENGTH_SHORT);
throw new NetworkOfflineException(); // try again later
}
}
}
ジョブを実行キューに積む
Job job = new Job(SimpleJobRunner.class);
JobService.post(context, job);
何らかの情報をジョブに伝える場合には、Mapに情報を入れてキューに積むと、実行時にその情報が使えます。
HashMap<String, Object> args = new HashMap<String, Object>();
args.put("id", 1000);
Job job = new Job(SimpleJobRunner.class, args);
よくありそうな質問
ジョブの結果が受け取れる時には受け取りたいけど、どうしたらいい?
Event bus使うと幸せです! otto や RxJava がおすすめ。
今後について
もっと使いやすいインターフェイスを考えていきたいですね。特にManifestの定義が必要のないところまで持って行きたい。 パラメータの受渡をもう少しかっこよくできればね。
近くに gradle&mavenのビルドシステムから簡単にdependency指定できるようにします。 (sonatype jira対応待ちです) Gradle/Mavenからdependency指定できるようになりましたー! プロジェクトGithubページに に書いてあります。
プルリクは大歓迎です!