イントロダクション
Androidアプリを開発していると、ユーザがアプリを閉じても何らかの処理をバックグラウンドで定期実行させたくなる場面が出てくる。具体的にはサーバーからデータを受信したり、逆に送信したり、不要なデータを削除したり、といったケースである。
やり方としてはいくつかあるので箇条書きにすると
- Serviceを起動してそこからTimerクラスで定期実行
- AlermManegerを使う(android4.0以降)
- JobSchedulerを使う(Android5.0以降)
- Workで定期実行(Android4.0以降)
1は初期のAndroidでは定番の方法だったようだが、Androidのバージョンが上がるたびにバッテリーとメモリの管理が厳しくなり、遅延が生じたり、システムがServiceを強制停止させたり、と動作が安定しなくなった。筆者も簡単なサンプルを作って動作を検証してみたが、アプリ起動中は意図した通りに動くけど、アプリを終了させるとServiceも何かのタイミングで落ちてしまった。
AlarmManegerは一定間隔でプログラムを実行するだけでなく、「何時何分に実行」と時刻を指定することができる。個人的には時計アプリを作った時に使用したことがある。スケジュールで起動→メインの処理を実行→再スケジュール、という処理を延々と繰り返すわけだ。これもAndroidにバッテリー消費を抑えるDozeモードが導入されてから実行のタイミングが不安定になった(とはいえ、なにがなんでも指定した時刻に実行するよう指定することもできる)。
JobSchedulerは比較的長い間隔で定期実行するのに向いているサービス。というのも実行間隔を15分未満には設定できないのでそれより短いスパンで実行したい場合には使えない。ネットワークの状態やバッテリーの状態を起動条件に指定できるので、自前でそれらのチェックをしなくて済むのは助かる。
Workは、JobSchedulerに似た仕組みで実行間隔を15分未満にはできない。起動条件も指定できる。使えるのがAndroid4.0以降と互換性が高いが、それもそのはずで、内部的にはAndroid5.0以降ではJobSchedulerを、Android4.0以上5.0未満ではAlarmManagerを使って機能を実現している。
この記事ではWorkを使って処理をバックグラウンドで定期実行する方法を記す。
Workの概要
事前準備
アプリbuild.gradleのdependenciesに次の一文を追加する。
プログラムをJavaで記述する場合
dependencies {
implementation "androidx.work:work-runtime:2.4.0"
}
プログラムをkotlinで記述する場合
dependencies {
implementation "androidx.work:work-runtime-ktx:2.4.0"
}
バージョン番号はこの記事を書いている時点での最新バージョンなので適時変更してください。
Workのオブジェクト
クラス | 説明 |
---|---|
WorkManager | Workの実行状況を監視する |
Worker | バックグラウンド処理の定義 |
WorkRequest | 実行条件やタイミングを記録。単体では使わず次の継承クラスを使う。 |
OneTimeWorkRequest | 一度だけ処理を実行するWorkRequest |
PeriodicWorkRequest | 定期的に処理を実行するWorkRequest |
Constraints | 実行条件の定義 |
Data | Workerに渡すデータを保存 |
処理の定義
バックグラウンドでどんな処理をさせるかはWorkerクラスを継承したクラスを作成し、doWork()メソッドをオーバーライドして、そこに処理を記述する。
doWork()の戻り値は以下の通り
戻り値 | 説明 |
---|---|
Result.success() | 処理成功 |
Result.failure() | 処理失敗 |
Result.retry() | 処理再実行 |
制約(起動条件)
Workerを実行するのに、必ずネットワークに繋がっていなければならないとか、デバイスがアイドル状態である(ユーザが操作していない)とか、起動するための前提条件があるときはConstraintクラスにフラグをセットしておく。
よく使うと思われる条件は次の通り
Constraintのメソッド | 説明 |
---|---|
setRequiredNetworkType | ネットワークの状態 |
setRequiresBatteryNotLow | バッテリー残量が少なくないか |
setRequiresCharging | 充電中であるかどうか |
setRequiresDeviceIdle | デバイスがアイドル状態か |
setRequiresStorageNotLow | ストレージの残り容量が少なくないか |
制約をセットしておけば、自前でデバイスの状態をチェックしなくてもよいため、コーディング量が減って可読性が高くなるので積極的に活用するべきだ。
データ
Workerは基本的にアクティビティなどのアプリ本体とは無関係に、システムがインスタンスを作成するのでコンストラクタ引数でデータを渡すことはできない。データを渡したい場合はDataクラスのインスタンスをWorkRequestにsetInputdata()メソッドで登録しておき、Workerが実行されたときにgetInputdate()メソッドで取得する。
スケジューリング
Workerをいつ実行するかはWorkRequestクラスのインスタンスを作成するときに指定する。
一回だけ実行する場合はOneTimeWorkRequestクラス、繰り返し実行する場合はPeriodicWorkRequestクラスを使用する。
制約やデータがあればWorkRequestにセットしてWrokManagerクラスのenqueue()メソッドでキューに登録してスケジューリングする。
WorkRequestのキャンセル
キューに登録されたWorkRequestはIDやタグを使ってキャンセルすることができる。IDはWorkRequestクラスのgetId()メソッドで取得でき、タグはあらかじめ任意の文字列をaddTag()メソッドで登録しておく。
WorkerManagerのメソッド | 説明 |
---|---|
cancelWorkById(uuid) | IDを指定してキャンセル |
cancelWorkByTag(tag) | タグを指定してキャンセル |
複数のWorkRequestに同じタグが登録されていたらその全てがキャンセルされる。
サンプルプログラム
以上のことを踏まえてWorkerでバックグラウンド処理を実行するアプリのサンプルを作ってみよう。
簡単な仕様
- アクティビティ上のボタンでバックグラウンド処理を起動。
- バックグラウンド処理では20分間隔でログを出力する
- アクティビティ上のボタンでバックグラウンド処理を停止。
レイアウト
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<Button
android:id="@+id/button1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:text="Worker Start"/>
<Button
android:id="@+id/button2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_below="@id/button1"
android:text="Worker Stop"/>
</RelativeLayout>
JAVAファイル
MainActivity.java
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
//定数
final private static String LOG_TAG = "worker_sample";
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button button1 = findViewById(R.id.button1);
button1.setOnClickListener(this);
Button button2 = findViewById(R.id.button2);
button2.setOnClickListener(this);
}
/* Workerを起動 */
private void startWrok (int num) {
//Workerに渡すデータを作成
Data data = new Data.Builder()
.putInt("num", num)
.build();
//制約(起動条件)を設定
// このサンプルでは制約なしでもいいが
// とりあえずバッテリー容量が少ないときはNGとした
Constraints constraints = new Constraints.Builder()
.setRequiresBatteryNotLow(true)
.build();
//WorkRequestの作成
// PeriodicWorkRequestを使って20分毎に定期実行
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder(
SampleWorker.class, 20, TimeUnit.MINUTES)
.setConstraints(constraints)
.setInputData(data)
.addTag(SampleWorker.WORK_TAG)
.build();
//キューに登録(スケジューリング)
WorkManager manager = WorkManager.getInstance(this);
manager.enqueue(request);
Log.d(LOG_TAG, "Worker scheduled");
}
/* Workerを停止 */
private void stopWork() {
WorkManager manager = WorkManager.getInstance(this);
manager.cancelAllWorkByTag(SampleWorker.WORK_TAG);
Log.d(LOG_TAG, "Worker stopped");
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.button1:
startWrok(2); //Workerを起動
break;
case R.id.button2:
stopWork(); //Workerのキャンセル
break;
default:
break;
}
}
}
SampleWorker,java
public class SampleWorker extends Worker {
//定数
final private static String LOG_TAG = "worker_sample";
final public static String WORK_TAG = "SampleWorkerTAG";
//コンストラクタ
public SampleWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
super(context, workerParams);
}
@NonNull
@Override
public Result doWork() {
//データを取得
Data data = getInputData();
int num = data.getInt("num", 1);
//出力テキストの作成
//numの個数だけ"Beautiful"というテキストを出力
StringBuilder sb = new StringBuilder();
for (int i=0; i<num; i++) {
sb.append("Beautiful ");
}
sb.append(getId().toString());
String output = sb.toString().trim();
//出力
Log.d(LOG_TAG,output); //ログに出力
return Result.success();
}
}
上のサンプルアプリを起動すると次のような画面が表示される
「WORKER START」ボタンをクリックするとWorkerがスケジューリングされ、20分毎にデバッグ用ログが出力される。「WORKER STOP」ボタンをクリックするとスケジューリングされているWorkerがキャンセルされる。ログはアクティビティ上には表示されないため何も動いていないように見えるが裏ではログが吐き出されている。
実行結果は次のようになった
13:37:15.860 5976-5976/net.fineblue206.workersample D/worker_sample: Worker scheduled 13:37:15.991 5976-6140/net.fineblue206.workersample D/worker_sample: Beautiful Beautiful 9077cb01-604f-4998-9009-a7b3067b2b61 13:57:16.072 5976-6310/net.fineblue206.workersample D/worker_sample: Beautiful Beautiful 9077cb01-604f-4998-9009-a7b3067b2b61 14:17:16.198 5976-6398/net.fineblue206.workersample D/worker_sample: Beautiful Beautiful 9077cb01-604f-4998-9009-a7b3067b2b61 14:21:22.545 5976-5976/net.fineblue206.workersample D/worker_sample: Worker stopped
スケジューリングされて即最初の処理が実行されて、その後20分毎に実行して、最後にキャンセルしているのがわかる。定期処理はアプリが終了しても実行されるので、それを望まないときは適切なタイミングでキャンセルしておく。
また、「WORKER START」を続けてクリックしたときは、バックグラウンド処理が複数走ってしまう。これを利用して疑似的に5分間隔とか15分より短いスパンで実行させることもできるが、実行間隔はあくまでも目安なので、デバイスの状態次第で遅延することもある。なのできっちり5分間隔にできるとは期待しないほうがいい。いろいろ実験してみたところ、デバイスがスリープ状態になると、定期実行は完全に止まり、スリープ解除された時点でキューに溜まっていた処理が一気に実行されるようである。すなわち2件バックグラウンド処理が登録されていたら、時間差で起動していたとしてスリープから復帰した時点で2つ同時に処理が走ってしまう。同じ処理をするプロセスが同時に動くとアプリクラッシュを引き起こしかねないので、重複して起動しないようチェックする必要がありそうだ。
というわけで、Workを使ってバックグラウンドで処理を実行する方法を紹介してみた。
重複起動の問題については別記事にするつもりだ。
参考資料
Google公式ドキュメント
https://developer.android.com/topic/libraries/architecture/workmanager?hl=ja
PeriodicWorkRequest request = new PeriodicWorkRequest.Builder( SampleWorker.class, 20, TimeUnit.MINUTES)
の「SampleWorker.class」でエラーが出ます。
デバックでは、実行可能なプログラムがありませんと表示されますが、何か対処方法はありませんでしょうか。
SampleWorkerクラスが定義されていないか、タイプミスかもしれません