Sunflowerリポジトリで学ぶJetPack〜WorkManager編

新型コロナの影響で自宅待機になってしまい、その間勉強するものとして`Sunflower`リポジトリを
勧めてもらいました。

JetPackのライブラリのうち、今回は`WorkManager`編です

尚、引用しているソースは明記しているところ以外は、基本的には全て`Sunflower`のリポジトリのものです。　

# 環境
* 確認時は`Android Studio`のバージョンは `3.6.2`を使用しました
* `JetPack`は`AndroidX`ライブラリを利用するので`Compile SDK`を`２８`以上にする必要があります

# そもそも`WorkManager`ってなに？
```
WorkManager API を使用すると、アプリが終了したりデバイスが再起動したりしても
実行することが要求される延期可能な非同期タスクのスケジュールを簡単に設定できます。
```

です！

# `WorkManager`の主な機能
## 機能
* API 14 までの下位互換性
    * API 23 以上が搭載されたデバイスでは `JobScheduler` を使用
    * API 14～22 が搭載されたデバイスでは `BroadcastReceiver` と `AlarmManager` を組み合わせて使用
* ネットワークの可用性や充電ステータスなどの処理の制約を追加する
* 非同期の 1 回限りのタスクや定期的なタスクのスケジュールを設定する
* スケジュール設定されたタスクの監視と管理を行う
* タスクを連携させる
* アプリやデバイスが再起動してもタスクを確実に実行する
* Doze モードなどの省電力機能に準拠する
(公式より）

この中だと、APIレベルが低いものでも同じように使えるというところと、`Dozeモード`に対応しているのがいいですね
（`Dozeモード中`にバックグラウンド処理が色々動かない、というのはAndroidあるあるだと思いますので。。。）

## 注意点
公式によると
---
WorkManager は、アプリが終了したりデバイスが再起動したりしても**確実に実行する**必要がある**遅延可能**なタスク（つまり、直ちに実行する必要がないタスク）を対象としています。次に例を示します。

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

---

つまり、確実に**定周期で何かをする**、などということには対応していない、ということですね。

# 利用する際に必要なこと

必要なことは下記の4点です

1. 依存関係の記載
1. バックグラウンドタスクの作成
1. タスクを実行する方法とタイミングを設定とタスクをシステムに引き渡す処理

# 依存関係の記載

## `build.gradle`に記載する内容

`build.gradle`には下記の依存関係の記載を行います。
(公式ドキュメントより）

```gradle:build.gradle
    dependencies {
      def work_version = "2.3.1"

        // (Java only)
        implementation "androidx.work:work-runtime:$work_version"

        // Kotlin + coroutines
        implementation "androidx.work:work-runtime-ktx:$work_version"

        // optional - RxJava2 support
        implementation "androidx.work:work-rxjava2:$work_version"

        // optional - GCMNetworkManager support
        implementation "androidx.work:work-gcm:$work_version"

        // optional - Test helpers
        androidTestImplementation "androidx.work:work-testing:$work_version"
      }
```
* 必須設定
    * `Java`
    * `Kotlin + Coroutines`
        * `Java`で使う場合は、`Java`の設定が、`Kotlin`で使う場合は`Kotlin`の設定が必須となります。
* オプション設定
    * `RxJava2`
    * `GCMNetworkManager`
    * `Test helpers`
        * これらはオプションなので、利用する場合は記載します。

## `Sunflower`リポジトリの場合
`Sunflower`リポジトリの場合はどのようになっているかを見てみましょう

```gradle:build.gradle
buildscript {
    // Define versions in a single place
    ext {
                    :
        // App dependencies
                    :
        workVersion = '2.1.0'
    }
                    :
}
```
```gradle:app/build.gradle
dependencies {
                        :
    implementation "androidx.work:work-runtime-ktx:$rootProject.workVersion"
                        :
    // Testing dependencies
                        :
    androidTestImplementation "androidx.work:work-testing:$rootProject.workVersion"
                        :
}
```
`Sunflower`リポジトリの場合は、下記設定となっていました。

* `Kotlin`で利用しています
* オプションは`Test helpers`を利用しています

# バックグラウンド タスクを作成する
それでは`Sunflower`のバックグラウンドタスクを見てみましょう。

```kotlin:SeedDatabaseWorker.kt（part1)
class SeedDatabaseWorker(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {
    override suspend fun doWork(): Result = coroutineScope {
                .
                .
                .
```

* タスクは通常は`Worker`クラスを使用して定義します
* `Workmanager`から提供されるバックグラウンドスレッドで`dowork()`メソッドが`同期的`に実行されます
* ただし、`Sunflower`リポジトリでは`CoroutineWorker`を利用しています。
    * こちらは`Kotlin`を使う際にオススメです
    * その場合`doWork()`メソッドは`suspend`関数になり、サブスレッドで実行されます。
        * 実行するスレッドは、`Worker`クラスとは異なり、`Configuration`で指定された`Executor`ではありません。
        * `Dispatchers.Default`で実行されます。


```kotlin:SeedDatabaseWorker.kt（part2)
        try {
            applicationContext.assets.open(PLANT_DATA_FILENAME).use { inputStream ->
                JsonReader(inputStream.reader()).use { jsonReader ->
                    val plantType = object : TypeToken<List<Plant>>() {}.type
                    val plantList: List<Plant> = Gson().fromJson(jsonReader, plantType)

                    val database = AppDatabase.getInstance(applicationContext)
                    database.plantDao().insertAll(plantList)

                    Result.success()
                }
            }
        } catch (ex: Exception) {
            Log.e(TAG, "Error seeding database", ex)
            Result.failure()
        }
    }

    companion object {
        private val TAG = SeedDatabaseWorker::class.java.simpleName
    }
}
```
* 以降には、バックグラウンドタスクで実施する処理が記載されています。
    * ここでは`assets`内のJsonファイルを読み込んだあと、パースしてデータベースに保存しています。
* `doWork()`メソッドは`Result`の各メソッドを返します。
    * `Result.success()`: タスクが正常に終了したかどうか
    * `Result.failure()`: タスクが失敗したかどうか
    * `Result.retry()`: 後でタスクを再試行する必要があるかどうか
        * ここでは、データベースに保存ができれば正常終了、例外が発生した場合は失敗となっています。
        
#  タスク実行方法とタイミングを設定およびタスクをシステムに引き渡す処理
## 概要
* `Worker(CoroutineWorker)`が作業単位を定義しますが、それに対し、`WorkRequest`は作業を実行する方法とタイミングを定義します。
* タスクには1回実行するものと、定期的に実行されるものがあります
    * 1回のもの　→　`OneTimeWorkRequest`
    * 定期的なもの　→　`PeriodicWorkRequest`
* `WorkRequest`を定義した後、`WorkManager`で`enqueue()`メソッドを使用してスケジュールを設定できます。

## `Sunflower`リポジトリ

```kotlin:AppDatabase.kt
        // Create and pre-populate the database. See this article for more details:
        // https://medium.com/google-developers/7-pro-tips-for-room-fbadea4bfbd1#4785
        private fun buildDatabase(context: Context): AppDatabase {
            return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
                    .addCallback(object : RoomDatabase.Callback() {
                        override fun onCreate(db: SupportSQLiteDatabase) {
                            super.onCreate(db)
                            val request = OneTimeWorkRequestBuilder<SeedDatabaseWorker>().build()
                            WorkManager.getInstance(context).enqueue(request)
                        }
                    })
                    .build()
        }
```
`Sunflower`リポジトリでは下記の処理になっています。

* データベース作成時に一度だけ実施する処理`OneTimeWorkRequestBuilder`で`SeedDatabaseWorker`を実施している。

# まとめ

* バックグラウンドタスクの作成は`Worker`クラスを作成する。`Kotlin`の場合は`CoroutineWorker`がおすすめ
* `Worker`クラスの`dowork()`メソッド内に実行する処理を書く。
* タスクの実行の際、1回しか実施しないことは、`OneTimeWorkRequest`、定期的に実施するものは`PeriodicWorkRequest`で実行する。

以上です！

# 参考サイト
* [Sunflowerリポジトリ](https://github.com/android/sunflower)
* [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager): 公式ドキュメント