[Android]WorkManagerでユーザを待たせないUIを実現する

## はじめに
仕事や日常で、ユーザが操作できない時間が長いアプリを触ることがある。気が短いので、操作できない区間が少しでもあるとイライラしてしまう。スマホアプリでは、安直にユーザ操作を禁止しないで欲しいという思いから、この記事を書く。

メール送信、掲示板の書き込み、つぶやきなどの投稿機能があるアプリを想像しながら読むと分かりやすいと思う。ユーザが操作できない時間が長いUI = ユーザを待たせるUIとする。

話が長いと感じるのであれば、「実践」だけ見れば良し。
後、Firestoreを使っていたり、ローカルDBの変化を監視する仕組みを使っているなら、WorkManager使わなくても同じことができるので、読む必要ないかも。

## ユーザを待たせるUIとそうでないUI
### ユーザを待たせるUI

**具体例**
1. (アプリ)投稿内容を入力する画面を表示する
2. (ユーザ)投稿内容を入力して、送信ボタンを押す
3. (アプリ)投稿処理を開始。ProgressDialog[^1]などを表示して、ユーザが操作できないようにする
4. (アプリ)投稿処理が終わったら、ProgressDialogなどを閉じて、ユーザの操作を受け付けるようにする。投稿したものが見れる一覧画面を表示する。

やや古いアプリや意識の低いアプリだとありがちな作り。
フリーズよりはましだが、結局は操作できない時間がある。

投稿完了までの時間が短いなら、それほどストレスも感じないので良い。
投稿完了までに時間がかかる場合は、他の操作をしたくなるので、ユーザ操作を禁止するのはいまいち。

### ユーザを待たせないUI

**具体例**

1. (アプリ)投稿内容を入力する画面を表示する
2. (ユーザ)投稿内容を入力して、送信ボタンを押す
3. (アプリ)投稿処理を開始。投稿一覧画面を表示する。
4. (ユーザ)他の投稿を眺めたり、新しく投稿内容を入力する
5. (アプリ)投稿処理が終わったとき、必要であれば、画面を更新する。投稿一覧画面であれば、投稿一覧画面の再読み込みなどをする。

デモ画像を示す。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/196890/26cf936e-5380-141c-774f-f0f5a699966f.gif" width="200" />

以下、このようなUIをWorkManagerで実現する方法を書く。

## WorkManagerで実現するユーザを待たせないUI

### WorkManagerとは
WorkManagerは、AndroidのJetPackの一種。アプリの画面のライフサイクルに紐付かないバックグラウンド処理をするのに使う。1回限りの処理、定期実行処理などができる。また、複数の処理をチェインさせたりも簡単にできる。

### WorkManagerを使う理由

WorkManagerを使ったバックグラウンド処理(Workerと呼ぶ)は、実行されることが保証されている。OneShotのWorkerの実行中にアプリのプロセスをタスクキルすると、一旦はWorkerの処理は止まるが、しばらくすると、Workerは再実行される。

この性質は、確実に実行したい処理があり、処理中でもユーザを待たせたくない状況に向いている。

### 実践
簡単なNote投稿アプリを例に、ユーザを待たせないUIの実践例を示す。

#### Step1. WorkManagerの導入

```gradle:app/build.gradle
implementation "androidx.work:work-runtime-ktx:2.4.0"
```

#### Step2. 投稿処理をWorkerのサブクラスとして実装する

```kotlin:
class PostWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) {
    override fun doWork(): Result {
        // 開始要求時に指定されたパラメータを取得する(後述)。
        val title = inputData.getString("title") ?: ""
        val description = inputData.getString("desc") ?: ""
        
        // 投稿処理。
        // Workクラスは、applicationContextにアクセスできる。
        val noteRepository = NoteRepository(
            RemoteNoteService(applicationContext)
        )
        noteRepository.add(
            Note(
                title,
                description
            )
        )

        // 成功を返す。異常時はResult.failure()を返す。
        return Result.success()
    }
}


```

#### Step3. Workerの開始を要求する

```kotlin

// WorkManagerのインスタンス取得にはContextを必要とするので、AndroidViewModelを使う。
class NoteEditorViewModel(
    application: Application,
): AndroidViewModel(application) {
    val title = MutableLiveData("")
    val description = MutableLiveData("")
    private val workManager = WorkManager.getInstance(application)

    // 画面の"投稿"ボタンが押されたときに呼ぶ。
    fun post() {
        val data = Data.Builder()
            .putString("title", title.value!!)
            .putString("desc", description.value!!)
            .build()

        // Noteの投稿は1回実行されれば良いので、OneTimeにする。
        val request = OneTimeWorkRequestBuilder<PostWorker>()
            .setInputData(data)   // Workerにパラメータを渡す
            .addTag("PostWorker") // Workerの実行状況の監視時に使うタグを指定。
            .build()

        workManager.enqueue(request)
    }
}
```

WorkManager#enqueueを呼ぶことで、1つ前で実装したWorkerの処理が開始される。
viewModelScopeと違って、ViewModelのライフサイクルとは紐付いていないので、ViewModelが破棄されてもWorkerは実行される。

#### Step4. Workerの終了を検知して画面を更新する

WorkManager.getWorkInfosByTagLiveData(tag)で、Workerの状況をLiveDataとして監視できる。Workerを開始した画面とは別の画面でも、この監視方法は使える。

```kotlin
class NotesViewModel(application: Application) : AndroidViewModel(application) {
    private val workManager = WorkManager.getInstance(application)

    ...

    // Worker開始時に指定したタグを使う。
    val saveWorkerInfos = workManager.getWorkInfosByTagLiveData("PostWorker")

    fun reload() {
        ...
    }
}

class NotesFragment : Fragment() {
    private val viewModel by viewModels<NotesViewModel>()

    ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        ...

        // PostWorkerが終了するたびに画面を再読み込みする。
        viewModel.saveWorkerInfos.observe(viewLifecycleOwner) { listOfWorkInfo ->
            val workInfo = listOfWorkInfo.firstOrNull()
            if (workInfo?.state?.isFinished == true) {
                viewModel.reload()
            }
        }
    }
}
```

以上!

## 補足

- [公式CodeLab](https://developer.android.com/codelabs/android-workmanager#0)
  - Workerのチェイン、キャンセルなどの説明あり
  - ここまで書いておいて何だが、個人のブログより公式のドキュメント読んだり、内部のコードを読むのが一番理解できる
- [サンプルアプリのコード](https://github.com/KamikazeZirou/no-wait-ui-sample)

[^1]: ProgressDialogはDeprecatedになっているが、安直にユーザの操作を禁止する風潮を感じる。実装は楽だが。