【Android】バックグラウンド処理をServiceからWorkManagerに移行してみた

# はじめに

皆さん、ごきげんよう！れぶです！
今回の記事は、バックグラウンド処理の実装についてです。

Android 8.0(Oreo)以降、バックグラウンド処理に対する厳しめな制限が導入されました。その結果、Service(特にバックグラウンドサービス)を使った実装をすると、動作が安定しません。詳しくは、[こちら](https://developer.android.com/about/versions/oreo/background?hl=ja#services)を参考にしてみてください。

なので、バックグラウンド処理を行う際には、Android Jetpack系の[WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager?hl=ja)を使うことをGoogleは推奨しています。このWorkManagerは、電池寿命の向上・互換性の高さ・様々な条件や制約を付与できるなど、メリットが沢山あります。API level 14から機能します。

そのような背景から、今回は`Service`と`WorkManager`を比較しながら、ボタンを押すと15分ごとにバックグラウンド処理を定期実行する実装方法を書いていきます。それでは参りましょう！！

# この記事の対象者

* 今までServiceを使ってバックグラウンド処理をしていたけど、WorkManagerを使った実装に移行したい方

# 開発環境

* Mac
* Android Studio 4.1.2
* Java 8
* compileSdkVersion 31
* minSdkVersion 16

# Serviceを使った実装

コードの主要部分がこちらになります。
Serviceを継承したクラスの中でTimerを使うことで、定期的なバックグラウンド処理を実現しています。

```MainService.java
    private Timer timer;

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {

        if (timer != null) {
            timer.cancel();
            timer = null;
        }

        //15分ごとに定期実行
        timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {

                //バックグラウンド処理
            }
        },0,900000);

        return START_NOT_STICKY;
    }
```
ボタンが選択されると、それぞれIntentでServiceの起動・停止を行います（フラグメント内）。

```MainFragment.java
    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        Button bt_start = view.findViewById(R.id.bt_start);
        Button bt_stop = view.findViewById(R.id.bt_stop);

        //startボタン選択時
        bt_start.setOnClickListener(new View.OnClickListener() {
         
            @Override
            public void onClick(View v) {

                //サービス起動
                Intent intent = new Intent(getActivity(),MainService.class);
                getActivity().startService(intent);
            }
        });

        //stopボタン選択時
        bt_stop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                //サービス停止
                Intent intent = new Intent(getActivity(),MainService.class);
                getActivity().stopService(intent);
            }
        });
    }
```

# WorkManagerを使った実装

## 登場人物

以下の3つのオブジェクトが中心です。

| オブジェクト | 説明 |
|:--|:--|
|Worker |バックグラウンド処理を定義   |
|WorkRequest   |処理の実行条件やタイミングを記録。一度だけ処理を実行するOneTimeWorkRequestと定期的に処理を実行するPeriodicWorkRequest（今回はこっち）がある。   |
|WorkManager  |Workの実行状況を監視   |

## 実装の流れ
###❶ build.gradleファイルに依存関係を追加

```build.gradle
dependencies {

    implementation "androidx.work:work-runtime:2.7.1"
}
```

現時点では、WorkManagerのバージョンは2.7.1です。適宜変更してください。

###❷ バックグラウンド処理を定義

```MainWorker.java
public class MainWorker extends Worker {

    public MainWorker(
            @NonNull Context context, @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @Override
    public Result doWork() {

        //バックグラウンド処理

        return Result.success();
    }   
}
```

Workerを継承したクラスの`doWorkメソッド`内に、バックグラウンド処理を書きます。Serviceを継承したクラスのonStartCommandメソッド内に書いていた内容が、まさにこの部分に該当します。

###❸ WorkRequestを作成
###❹ WorkManagerに❸を追加or削除

```MainFragment.java
　　　　　　　　@Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        Button bt_start = view.findViewById(R.id.bt_start);
        Button bt_stop = view.findViewById(R.id.bt_stop);

        //startボタン選択時
        bt_start.setOnClickListener(new View.OnClickListener() {
            @RequiresApi(api = Build.VERSION_CODES.O)
            @Override
            public void onClick(View v) {

                //15分おきに定期実行を開始
                //Wi-Fiに繋がっている、かつ電池残量低下モードになっていない場合のみ
                Constraints constraints = new Constraints.Builder()
                        .setRequiredNetworkType(NetworkType.UNMETERED)
                        .setRequiresBatteryNotLow(true)
                        .build();
                PeriodicWorkRequest workRequest = new PeriodicWorkRequest.Builder(
                        MainWorker.class, Duration.ofMinutes(15)
                )
                        .setConstraints(constraints)
                        .addTag("タグ名")
                        .build();
                WorkManager.getInstance(getActivity()).enqueue(workRequest);
            }
        });

        //stopボタン選択時
        bt_stop.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                //定期実行をキャンセル
                WorkManager.getInstance(getActivity()).cancelAllWorkByTag("タグ名");
            }
        });
    }
```

IntentでServiceの起動・停止を行なっていた箇所がここに該当します。startボタンで定期実行を開始し、stopボタンで停止しています（フラグメント内）。
`PeriodicWorkRequest`を作成する際にタグを追加しているのは、どの定期実行をキャンセルするかを識別するためです。

`WorkManager`はServiceと違い、様々な条件や制約を付与することができます。今回のWorkManagerのコードでは、ConstraintsオブジェクトをWorkRequestに登録し、Wi-Fiとバッテリーモードの状態による制限を加えています。

## 備考

今回のサンプルはあくまで一例です。他にも、

* 定期実行の間隔の変更（最小時間は15分）
* 一回のみの処理実行
* Workerへのデータの受け渡し
* 充電や端末の状態などによる制約の付与

などなど、沢山の条件を設定することが可能です。

詳しくは、[公式ドキュメント](https://developer.android.com/topic/libraries/architecture/workmanager/how-to/define-work?hl=ja)や[こちらの記事](https://qiita.com/naoi/items/e08f081fe42ef3694046)を参考にしてみてください。自身も非常に参考にさせて頂きました。

# WorkManagerの使い所

[公式ドキュメント](https://developer.android.com/topic/libraries/architecture/workmanager?hl=ja)で以下のように述べられています。これが基本原則です。

> WorkManagerは、ユーザーが画面から移動した場合、アプリが終了した場合、デバイスが再起動された場合でも、**確実に実行する必要がある処理**を対象としています。次に例を示します。

> * ログやアナリティクスをバックエンドサービスに送信する
* アプリデータをサーバーと定期的に同期する

何がなんでも裏で動作させたい処理に`WorkManager`を使うことが推奨されています。
他にも、データのレポーティングや画像の保存、ツイートなどが挙げられます。

逆に非推奨な処理を以下にまとめました。

| 処理 | 代替策 |
|:--|:--|
| ユーザーが画面を離れた時に、即時実行したタスクが終了する可能性がある処理  | Kotlin Coroutine（Javaの場合は、Executorなど）  |
| アラームやカレンダーなど、正確な時間どおりに実行する必要がある処理  | AlarmManager  |
※ メディア再生にはForegroundServiceを使うことが推奨されていましたが、Android 12以降、ForegroundServiceの制限ができました。その代替策として、WorkManagerが挙げられていました。詳しくは、[この記事](https://developer.android.com/about/versions/12/foreground-services?hl=ja)をご覧ください。

# おわりに

今回は、バックグラウンド処理をServiceからWorkManagerに変えて実装する方法を中心にまとめていきました。

基本は4ステップで出来るので、意外と簡単かなと思います。条件や制約次第で、多様なパターンのバックグラウンド処理の仕方があるので、各々試してみてください。その際には是非、適切な場面でWorkManagerを使えているかを意識してみてください！

なお、今回自身がこの記事を書くにあたり、ボタンを押すとバックグラウンド状態で通知が15分ごとに届くサンプルプログラムを作成しました。その際に書いたコードを以下のGitHubに載っけているので、参考の一つにしてみてください。

[https://github.com/seiyaleb/BackGroundTrialApp](https://github.com/seiyaleb/BackGroundTrialApp)

# 参考書籍・参考サイト
* 基礎＆応用力をしっかり育成！ Androidアプリ開発の教科書 なんちゃって開発者にならないための実践ハンズオン（書籍）
* [WorkManagerでタスクのスケジュールを設定する](https://developer.android.com/topic/libraries/architecture/workmanager?hl=ja)(公式ドキュメント)
* [WorkManagerの概要](https://developer.android.com/topic/libraries/architecture/workmanager/basics?hl=ja)(公式ドキュメント)
* [WorkRequestの定義](https://developer.android.com/topic/libraries/architecture/workmanager/how-to/define-work?hl=ja)(公式ドキュメント)
* [バックグラウンド処理ガイド](https://developer.android.com/guide/background?hl=ja)(公式ドキュメント)
* [いまさらWorkManager〜概要編〜](https://qiita.com/naoi/items/e08f081fe42ef3694046)
* [[Android]Workでバックグラウンド処理を定期実行する](http://www.fineblue206.net/archives/817)
* [WorkManagerの内部を見てみた](https://mikan-tech.hatenablog.jp/entry/2021/09/10/170000)
* [WorkManager](https://speakerdeck.com/takahirom/workmanager)