いまさらWorkManager〜基本編⑥（唯一無二のWorker）〜

# はじめに
この[記事](https://qiita.com/naoi/items/e08f081fe42ef3694046)で説明した通り、様々な機能を活用していきましょう。
WorkRequestをキューに追加したにもかかわらず、処理によってはもう一度キューに今一度追加してしまい、思わぬ不具合につながる事もあります。
アプリではキューに1つだけにしたい！！ってこともあります。
その様な要望にも答えられるようにWorkManagerは機能を提供しています。
今回はキューに追加するWorkRequestをユニークにする方法を説明します。

# 動作環境
この記事の動作環境は以下のとおりです。

* Android Studio：3.3
* Kotln：1.3.11
* Open JDK:1.8
* compileSdkVersion：28
* targetSdkVersion：28
* minSdkVersion：19

# 目標
キューに格納されているWorkRequestは1つだけにする！

# 完成イメージ
今回は、定期実行と複数回実行の2つを用意して、実行しています。
見た目ではこれまでと変わらないので、あまり面白味はないと思います。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99960/898c1c15-58d2-ac97-d414-f712ed055dee.gif" width="30%">

# 概要
詳細な説明の前に、キューないでユニークにする方法の概要を理解しておきましょう。
キューないでユニークにするには以下の内容を理解する必要があります。

* キューないでユニークにするメソッドによるWorkRequestの追加
* 追加するWorkRequestに名前をつける
* すでに同じ名前のWorkRequestが存在する場合の挙動の設定

図にしてみました。


![ユニークなWork.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/99960/0ca8f856-3772-3016-a334-5e34b3bc449a.png)


# 説明
## ユニークなWorkRequestの追加

キューないでユニークにするためには以下のメソッドを呼び出して追加します。

* enqueueUniqueWork
* enqueueUniquePeriodicWork

WorkRequestの種類によって呼び出すメソッドが異なります。
1回のみの実行であれば、 **enqueueUniqueWork** 。繰り返し実行する場合は、 **enqueueUniquePeriodicWork** です。

それではそれぞれのメソッドの詳細を確認してみましょう。

 **enqueueUniqueWork** 

|メソッド名|引数|
|---|---|
|enqueueUniqueWork|String uniqueWorkName,<br> ExistingWorkPolicy existingWorkPolicy, <br>OneTimeWorkRequest work|
|enqueueUniqueWork|String uniqueWorkName, <br>ExistingWorkPolicy existingWorkPolicy, <br>List&lt;OneTimeWorkRequest&gt; work|

メソッドが2つありますが、第③引数のWorkRequestが単一か複数の違いでしかありません。
それでも、引数の詳細を確認しましょう。

|引数|型|説明|
|---|---|---|
|第1引数|String|WorkRequestをユニークにするための名前|
|第2引数|ExistingWorkPolicy|同じ名前でキューに追加されたときの挙動|
|第3引数|OneTimeWorkRequest or &lt;ListOneTimeWorkRequest&gt;| WorkRequestのオブジェクト|

今回のポイントは第1引数と第2引数にありそうです。


第1引数と第2引数の説明の前に、実装例を確認してみましょう。

```kotlin
val request = OneTimeWorkRequest.Builder(MyWorker::class.java)
    .apply {
        ・・・省略・・・
    }.build()

manager.enqueueUniqueWork(onetimeWorkName, ExistingWorkPolicy.KEEP, request)
```


enqueueUniquePeriodicWorkメソッドも確認しておきます。
 **enqueueUniquePeriodicWork** 

|メソッド名|引数|
|---|---|
|enqueueUniquePeriodicWork|String uniqueWorkName, <br>ExistingPeriodicWorkPolicy existingPeriodicWorkPolicy, <br>PeriodicWorkRequest periodicWork|

enqueueUniqueWorkメソッドとよく似ています。

|引数|型|説明|
|---|---|---|
|第1引数|String|WorkRequestをユニークにするための名前|
|第2引数|ExistingPeriodicWorkPolicy|同じ名前でキューに追加されたときの挙動|
|第3引数|PeriodicWorkRequest| WorkRequestのオブジェクト|

いろいろ気になる事もあると思いますが、実装例も確認してみましょう。

```kotlin
val request = PeriodicWorkRequest.Builder(MyWorker::class.java, 20, TimeUnit.MINUTES)
    .apply {
       ・・・省略・・・
        }.build()
        setInputData(data)
    }.build()

manager.enqueueUniquePeriodicWork(periodicWorkName, ExistingPeriodicWorkPolicy.REPLACE, request)
```

enqueueUniqueWorkメソッドと比べてみると第2引数の型が明らかに違いますね。

第2引数はどちらもEnum型です。

それぞれの持てる値と説明を並べてみます。

 **ExistingWorkPolicy** 

|値|説明|
|---|---|
| APPEND |同じ名前で追加されたWorkRequestの状態が完了状態でない場合は、追加されたWorkRequestをペンディング中のWorkRequestの子供として追加し、順次実行されます。|
| KEEP|同じ名前で追加されたWorkRequestの状態が完了状態でない場合は、追加されません。|
| REPLACE|同じ名前で追加されたWorkRequestの状態が完了状態でない場合は、現在キューに入っているWorkRequestをキャンセル後削除し、追加されたWorkRequestをキューに追加します。|

 **ExistingPeriodicWorkPolicy** 

|値|説明|
|---|---|
| KEEP|同じ名前で追加されたWorkRequestの状態が完了状態でない場合は、追加されません。|
| REPLACE|同じ名前で追加されたWorkRequestの状態が完了状態でない場合は、現在キューに入っているWorkRequestをキャンセル後削除し、追加されたWorkRequestをキューに追加します。|

繰り返し実行の場合、APPENDは存在しません。

最後に、第1引数ですが、これはIDやタグとは別物になり、キューに追加されるWorkRequestを表す値です。そのため、IDやタグを指定した状態でも、関係なくユニークになるような文字列を指定します。


# 実装コード
この記事で動作確認したコードをすべて掲載します。
AndroidManifest.xmlはテンプレートで作成された状態から変更していないため、省略します。

```groovy:build.gradle
apply plugin: 'com.android.application'

apply plugin: 'kotlin-android'

apply plugin: 'kotlin-android-extensions'

android {
    compileSdkVersion 28
    defaultConfig {
        applicationId "jp.co.casareal.workmanageruniquework"
        minSdkVersion 19
        targetSdkVersion 28
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])
    implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
    implementation 'com.android.support:appcompat-v7:28.0.0'
    implementation 'com.android.support.constraint:constraint-layout:1.1.3'
    testImplementation 'junit:junit:4.12'
    androidTestImplementation 'com.android.support.test:runner:1.0.2'
    androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2'
    def work_version = "1.0.0"

    implementation "android.arch.work:work-runtime-ktx:$work_version"

}
```

```xml:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".MainActivity">
    <EditText
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:inputType="textPersonName"
            android:text="Casareal"
            android:ems="10"
            android:id="@+id/editInput"
            app:layout_constraintTop_toTopOf="parent" android:layout_marginEnd="8dp"
            app:layout_constraintEnd_toEndOf="parent" android:layout_marginStart="8dp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintBottom_toTopOf="@+id/btnStart"/>
    <Button
            android:text="WorkStart"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btnStart"
            app:layout_constraintStart_toStartOf="parent"
            android:layout_marginStart="8dp" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"
            app:layout_constraintTop_toBottomOf="@+id/editInput"
            android:layout_marginBottom="291dp" app:layout_constraintBottom_toTopOf="@+id/btnOnetimeStart"/>
    <Button
            android:text="oneTimeButton"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:id="@+id/btnOnetimeStart" app:layout_constraintEnd_toEndOf="parent" android:layout_marginEnd="8dp"
            app:layout_constraintStart_toStartOf="parent" android:layout_marginStart="8dp"
            app:layout_constraintTop_toBottomOf="@+id/btnStart"
            android:layout_marginBottom="32dp" app:layout_constraintBottom_toBottomOf="parent"
            android:layout_marginTop="16dp"/>
</android.support.constraint.ConstraintLayout>
```

```kotlin:MyWorker.kt
package jp.co.casareal.workmanageruniquework

import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import android.support.v4.app.NotificationCompat
import androidx.work.Worker
import androidx.work.WorkerParameters
import java.text.SimpleDateFormat

class MyWorker(cxt: Context, params: WorkerParameters) : Worker(cxt, params) {
    val notificationManager =
        applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
    // カテゴリー名（通知設定画面に表示される情報）
    val name = "通知のタイトル的情報を設定"
    // システムに登録するChannelのID
    val id = "casareal_chanel"
    // 通知の詳細情報（通知設定画面に表示される情報）
    val notifyDescription = "この通知の詳細情報を設定します"

    private val simpleDateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")

    companion object {
        var nid = 1
        var count = 1
    }


    init {
        // Channelの取得と生成
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            notificationManager.getNotificationChannel(id) == null
            val mChannel = NotificationChannel(id, name, NotificationManager.IMPORTANCE_HIGH)
            mChannel.apply {
                description = notifyDescription
            }
            notificationManager.createNotificationChannel(mChannel)
        }
    }

    override fun doWork(): Result {


        val notification = NotificationCompat.Builder(applicationContext, id).apply {
            setContentText("受け取ったメッセージ:${inputData.getString("message")},${count}回目のメッセージ")
            setSmallIcon(R.drawable.ic_launcher_background)
        }

        notificationManager.notify(MyWorker.nid, notification.build())

        return Result.success()
    }
}
```

```kotlin:MainActivity.kt
package jp.co.casareal.workmanageruniquework

import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import androidx.work.*
import kotlinx.android.synthetic.main.activity_main.*
import java.util.concurrent.TimeUnit

class MainActivity : AppCompatActivity() {

    private val manager = WorkManager.getInstance()
    private val periodicWorkName = "periodicmywork"
    private val onetimeWorkName = "onetimemywork"


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)


        btnOnetimeStart.setOnClickListener {
            val request = OneTimeWorkRequest.Builder(MyWorker::class.java)
                .apply {
                    val data = Data.Builder().apply {
                        putString("message", editInput.text.toString())
                    }.build()
                    setInputData(data)
                }.build()

            manager.enqueueUniqueWork(onetimeWorkName, ExistingWorkPolicy.KEEP, request)
        }

        btnStart.setOnClickListener {


            val request = PeriodicWorkRequest.Builder(MyWorker::class.java, 20, TimeUnit.MINUTES)
                .apply {
                    val data = Data.Builder().apply {
                        putString("message", editInput.text.toString())
                    }.build()
                    setInputData(data)
                }.build()

            manager.enqueueUniquePeriodicWork(periodicWorkName, ExistingPeriodicWorkPolicy.REPLACE, request)
        }
    }
}
```

# まとめ
キュー内でユニークにするには、WorkManagerの下記のメソッドを呼び出すだけで実現可能な事が理解できました。これらをうまく利用することによって、WorkRequestの輻輳を防ぐことができます。

* enqueueUniqueWork
* enqueueUniquePeriodicWork