WorkManager-version2.1.0-rc01の実践と知見

WorkManagerの使い方と得られた知見を忘れないようにまとめました。
WorkManagerの導入を検討している方などに参考になると嬉しいです。

Versionは、2019/6/27にリリースされている現時点で最新の2.1.0-rc01です。
https://developer.android.com/jetpack/androidx/releases/work

# 本記事の要約

- WorkManagerを使うと、非同期処理が簡単:thumbsup:
- 1回だけの処理も、1時間ごとなど繰り返し処理も簡単にできる:thumbsup:
- ただ、時間間隔には幅があるので、「この時間に絶対送りたい！」という要件には不向き
- RETRYの挙動についてはよく理解しておいた方がよい（バグの元凶）
- デバッグする際にはWorkInfoが使えて、今どういうステータスにあるのか確認できる:thumbsup:
- Version2.1.0からテストがしやすくなった！:thumbsup:

# 準備
まず、WorkManagerを使えるようにするために、AndroidプロジェクトにWorkManagerライブラリを追加します。
※WorkManagerには、compileSdkバージョン28以上が必要です。

```build.gradle
implementation 'androidx.work:work-runtime:2.1.0-rc01'
implementation 'androidx.work:work-testing:2.1.0-rc01'
implementation 'androidx.work:work-runtime-ktx:2.1.0-rc01'
```
# WorkManagerについて
WorkManagerは、Background作業を行いたい際に使用されるライブラリです。
内部的に端末のAPIレベルに合わせて、ライブラリを適切に選択してくれるAPIです。

- 選択されるライブラリ
    - API 23+にJobScheduler
    - API 14-22用のカスタムAlarmManager + BroadcastReceiver

なので、同じWorkManagerを使用していても、APIレベルによって内部的な処理は異なります。
そのほか詳細はドキュメントを読んでください。

公式ドキュメントは以下です。
https://developer.android.com/topic/libraries/architecture/workmanager/basics#concepts

# WorkManagerの基本的な構成
構成を知っていれば、ここは読み飛ばしてもらっても大丈夫です。

- Worker

    - このクラスを継承したクラスを作成する

- WorkRequest
    - OneTimeWorkRequestまたはPeriodicWorkRequestを使用する
        - OneTimeWorkRequest
            - Workerを**１度だけ実行したい**ときに使用する
        - PeriodicWorkRequest
            - Workerを**繰り返し、定期実行したい**ときに使用する

- WorkManager

    - バックグラウンドで実行する作業をエンキューする

- WorkInfo
    - タスクに関する情報
    - LiveDataでタスクの状態の観察、完了後の戻り値を取得できる


# 実践
## OneTimeWorkRequestを使う
では実際にOneTimeWorkRequestを使ってみます。

今回は、画面にボタンが１つあり、そのボタンをタップするとWorkerがすぐに実行される簡単なサンプルを作ってみました。

はじめに、レイアウトです。
レイアウトはボタンが１つあるだけのレイアウトを作成します。

```activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">

    <Button
            android:id="@+id/workerButton"
            android:layout_height="wrap_content"
            android:layout_width="wrap_content"
            android:text="Workerのテスト"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintLeft_toLeftOf="parent"
            app:layout_constraintRight_toRightOf="parent"
            app:layout_constraintTop_toTopOf="parent">
    </Button>
    
</androidx.constraintlayout.widget.ConstraintLayout>
```
次に、Workerを継承したMyWorkerクラスを作成します。
doWork()の中にWorkerが起動したときに実行したい処理を書きます。
今回の場合は、Logを仕込んでいます。

```MyWorker.kt
class MyWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {

    companion object {
        const val MY_WORKER = "com.example.workmanager.MyWorker"
        const val WORKER_NAME = "MyWorker"
    }
    override fun doWork(): Result {
        Log.d("hanakoLog", "doWork！")
        return Result.success()
    }
}
```

最後に、MainActivityです。
ボタンがタップされたタイミングで、OneTimeWorkRequestをWorkManagerにセットします。

```MainActivity.kt
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.Observer
import androidx.work.*
import com.example.workmanager.MyWorker.Companion.MY_WORKER
import kotlinx.android.synthetic.main.activity_main.*
import java.util.concurrent.TimeUnit

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        WorkManager.initialize(this, Configuration.Builder().build())

        workerButton.setOnClickListener {
            val workManager = WorkManager.getInstance(applicationContext)
            val request = OneTimeWorkRequest
                .Builder(MyWorker::class.java)
                .addTag(MY_WORKER)
                .build()
            workManager.enqueueUniqueWork(MyWorker.WORKER_NAME,
                ExistingWorkPolicy.REPLACE,
                request)
            workManager.getWorkInfoByIdLiveData(request.id).observe(this, Observer { workInfo ->
                    Log.d("hanakoLog", workInfo.state.name)
                    Log.d("hanakoLog", workInfo.id.toString())
                    Log.d("hanakoLog", workInfo.tags.toString())
            })
        }
    }
}
```
※ここでデバッグする際のチップス:point_up::sparkles:
workManager.getWorkInfoByIdLiveData(request.id)をobserveすることで、リクエストのstatusなどを確認できます。今回は、リクエストのstatus、id、タグをLogで確認します。Workerの動作を確認する際にこれは便利です。

以上で実装完了です！:ok_hand:
では、Logを確認してみましょう。

```
2019-07-03 22:47:05.912 12670-12670/com.example.workmanager D/hanakoLog: ENQUEUED
2019-07-03 22:47:05.914 12670-12670/com.example.workmanager D/hanakoLog: aa67594e-b05e-4c07-9e48-391948279864
2019-07-03 22:47:05.916 12670-12670/com.example.workmanager D/hanakoLog: [com.example.workmanager.MyWorker]
2019-07-03 22:47:05.963 12670-13502/com.example.workmanager D/hanakoLog: doWork！
2019-07-03 22:47:06.013 12670-12670/com.example.workmanager D/hanakoLog: SUCCEEDED
2019-07-03 22:47:06.014 12670-12670/com.example.workmanager D/hanakoLog: aa67594e-b05e-4c07-9e48-391948279864
2019-07-03 22:47:06.014 12670-12670/com.example.workmanager D/hanakoLog: [com.example.workmanager.MyWorker]
```
Logから、

- ボタンをタップすると
- MyWorkerの
- a67594e-b05e-4c07-9e48-391948279864というidのリクエストが
- ENQUEUEDになり
- doWork()が実行され
- SUCCEEDEDされている

様子がわかります。

OneTimeWorkRequestは、PeriodicWorkRequestに比べて動きがシンプルで動作の確認もしやすいです。

ちなみにdoWork()が実行されるまでに遅延時間を指定することなども可能です。
詳しくはドキュメントを読んでみてください！
https://developer.android.com/reference/androidx/work/OneTimeWorkRequest

## PeriodicWorkRequestを使う
次に、定期的にdoWork()を実行するPeriodicWorkRequestを実際にやってみます。

今回は、15分に1回Workerが動作するようにしたいと思います。レイアウトとMyWorkerクラスはOneTimeWorkRequestのときと一緒です。MainActivityのみを以下のように修正します。

```MainActivity.kt
class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        WorkManager.initialize(this, Configuration.Builder().build())

        workerButton.setOnClickListener {
            val workManager = WorkManager.getInstance(applicationContext)
            val request = PeriodicWorkRequest
                .Builder(MyWorker::class.java, PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS, PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS)
                .addTag(MY_WORKER)
                .build()
            workManager.enqueueUniquePeriodicWork(MyWorker.WORKER_NAME,
                ExistingPeriodicWorkPolicy.REPLACE,
                request)

            workManager.getWorkInfoByIdLiveData(request.id).observe(this, Observer { workInfo ->
                    Log.d("hanakoLog", workInfo.state.name)
                    Log.d("hanakoLog", workInfo.id.toString())
                    Log.d("hanakoLog", workInfo.tags.toString())
            })
        }
    }
}
```
PeriodicWorkRequest.Builder()の中では、以下の3つを指定しています。

- 実行するWorkerクラス

- 繰り返す周期(repeat interval)
    - Workerを実行したい間隔を指定します。今回は15分に1回です。デフォルト値はMIN_PERIODIC_INTERVAL_MILLIS(15分)でそれより短い時間を指定しても、15分になります。
    
- jobが実行される最小駆動時間(flex interval)
    - WorkManagerは、**repeat intervalで例え1時間に指定しても、きっちり1時間で実行されるわけではありません:no_good:** 1時間ごとにここで指定するflex intervalの範囲内で実行されます。
(とはいえ、そこもきっちり保証されるわけではなく、あくまで目安と思っておいた方がよいです)
デフォルト値はMIN_PERIODIC_FLEX_MILLIS(5分)でそれより短い時間を指定しても、5分になります。
今回はデフォルトの値を設定しているので、1時間間隔で5分以内にリクエストするコードになっています。

以上で実装完了です！:ok_hand:
では、Logを確認してみましょう。

```
2019-07-07 12:29:56.284 18571-18571/com.example.workmanager D/hanakoLog: ENQUEUED
2019-07-07 12:29:56.293 18571-18571/com.example.workmanager D/hanakoLog: cabde0e3-3a17-47ff-9ee6-a4c833cfcd8a
2019-07-07 12:29:56.297 18571-18571/com.example.workmanager D/hanakoLog: [com.example.workmanager.MyWorker]
2019-07-07 12:45:56.100 18571-18571/com.example.workmanager D/hanakoLog: doWork!
2019-07-07 13:03:50.254 18571-18571/com.example.workmanager D/hanakoLog: doWork!
2019-07-07 13:19:82.124 18571-18571/com.example.workmanager D/hanakoLog: doWork!
```
Logから

- ボタンをタップすると
- MyWorkerの
- a67594e-b05e-4c07-9e48-391948279864というidのリクエストが
- ENQUEUEDになり
- その約15分後
- doWork()が実行され
- またその約15分後
- doWork()が実行され
- またその約15分後
- doWork()が実行され・・・

と、繰り返しWorker様子が動いている様子がわかります。
flex intervalintervalの説明で書きましたが、指定した15分できっちり時間通りには動いていないことが見てわかりますね。
なので、この時間に絶対送りたい！という要件にはWorkManagerのPeriodicWorkRequestは向いていないと思います。概ねx時間、x分位の間隔で定期実行させたいな～っという場合に使うとよいと思います。

# 知見
## 知見①
OneTimeWorkRequestには、ENQUEUED、SUCCEEDED、FAILED、RETRY、CANCELEDステータスがあります。
OneTimeWorkRequestの実行を確認する際は、workInfo.state.namenameでどのステータスにいるのかを確認できます。

一方、PeriodicWorkRequestには、SUCCEEDED、FAILED、RETRYステータスはありません。
基本ENQUEUEDのみで、明示的にキャンセルするとCANCELEDになります。なので、PeriodicWorkRequestの実行を確認する際、SUCCEEDED、FAILED、RETRYステータスステータスは取れないことに注意が必要です。

## 知見②
RETRYの挙動は複雑で要注意。:rolling_eyes:

- BackoffPolicy
    - デフォルトでBackoffPolicy.EXPONENTIALという設定がされているようですが、これは、リトライ間隔を指数関数的に増加させていく設定です。大抵の場合、このデフォルト設定で問題はないですが、リトライが繰り返された場合、最終的に数日後などにリトライされる可能性があることを理解しておきましょう。
    
- リトライ回数
    - リトライ回数の最大値を設定することはできません。回数制限したい場合は、Workerクラス内でgetRunAttemptCountを呼ぶと、現在の試行回数を取得可能なので、これを使用して、任意の回数に達したら、FailureさせてRetlyを防ぐとよいそうです。ちなみにgetRunAttemptCountは、一度もリトライされずに問題なく実行されている場合は、0です。

## 知見③
1.0.0alphaから2.1.0-rc01にあげたことでことで、変わったことがありました。
それは、WorkManagerのテストコードです。

公式ドキュメントに記載されている通り、2.1.0からTestWorkerBuilderとTestListenableWorkerBuilderが提供されて、テストコードが書きやすくなっていました！
以下、公式ドキュメントの引用です。
>　WorkManager 2.1.0 provides new TestWorkerBuilder and TestListenableWorkerBuilder classes, which let you test the business logic in your workers without having to initialize WorkManager with WorkManagerTestInitHelper.

詳細については以下をご覧ください。
https://developer.android.com/topic/libraries/architecture/workmanager

WorkManagerのテストについては、改めて書きたいと思います。

# おわりに
workManagerの動作を実際に確認することができました。
非同期処理なので動作確認やテストが少しやりにくい所もありますが、versionが上がるたびに少しずつバグFIXされ、テストもしやすいようになってきています。
ぜひ、まだ1.0.0alphaなど使用している場合は2.1.0rc-01にバージョンを上げるのがおススメです:thumbsup_tone1:（WorkManager関連の内部的なバグでクラッシュが多かったのが、2.1.0にあげてからだいぶ減りました。）

# 参考にした文献
- https://medium.com/androiddevelopers/workmanager-periodicity-ff35185ff006
- https://medium.com/androiddevelopers/workmanager-meets-kotlin-b9ad02f7405e