Firebase JobDispatcher から WorkManager への移行

WorkManager は、Android で遅延可能なバックグラウンド処理をスケジューリングし実行するためのライブラリです。Firebase JobDispatcher の代わりに使用することが推奨されています。以下のガイドでは、Firebase JobDispatcher による実装を WorkManager に移行する手順を説明します。

Gradle のセットアップ

JobService から Worker へ

FirebaseJobDispatcher は、実行する必要がある処理を定義するためのエントリ ポイントとして、JobService のサブクラスを使用します。JobService を直接使用していること、あるいは SimpleJobService を使用していることもあります。

JobService は以下のようになっています。

Kotlin

    import com.firebase.jobdispatcher.JobParameters
    import com.firebase.jobdispatcher.JobService

    class MyJobService : JobService() {
        override fun onStartJob(job: JobParameters): Boolean {
            // Do some work here
            return false // Answers the question: "Is there still work going on?"
        }
        override fun onStopJob(job: JobParameters): Boolean {
            return false // Answers the question: "Should this job be retried?"
        }
    }
    

Java

    import com.firebase.jobdispatcher.JobParameters;
    import com.firebase.jobdispatcher.JobService;

    public class MyJobService extends JobService {
        @Override
        public boolean onStartJob(JobParameters job) {
            // Do some work here

            return false; // Answers the question: "Is there still work going on?"
        }

        @Override
        public boolean onStopJob(JobParameters job) {
            return false; // Answers the question: "Should this job be retried?"
        }
    }
    

SimpleJobService を使用している場合は、@JobResult int 型を返す onRunJob() をオーバーライドしています。

主な違いは、JobService を直接使用している場合、onStartJob() はメインスレッドから呼び出され、処理をバックグラウンド スレッドにオフロードするのはアプリの責任になるという点です。これに対して、SimpleJobService を使用している場合、バックグラウンド スレッドで処理を実行するのはこのサービスの責任になります。

WorkManager にも同様のコンセプトがあります。WorkManager の基本の処理単位は ListenableWorker です。ほかにも、ワーカーには、WorkerRxWorkerCoroutineWorker(Kotlin のコルーチンを使用する場合)のような便利なサブタイプもあります。

JobService を ListenableWorker に変換する

JobService を直接使用している場合、それに対応するワーカーは ListenableWorker です。SimpleJobService を使用している場合は、Worker を使用する必要があります。

上の MyJobService の例を使用して、それを ListenableWorker に変換する方法を説明します。

Kotlin

    import android.content.Context
    import androidx.work.ListenableWorker
    import androidx.work.ListenableWorker.Result
    import androidx.work.WorkerParameters
    import com.google.common.util.concurrent.ListenableFuture

    class MyWorker(appContext: Context, params: WorkerParameters) :
        ListenableWorker(appContext, params) {

        override fun startWork(): ListenableFuture<ListenableWorker.Result> {
            // Do your work here.
            TODO("Return a ListenableFuture<Result>")
        }

        override fun onStopped() {
            // Cleanup because you are being stopped.
        }
    }
    

Java

    import android.content.Context;
    import androidx.work.ListenableWorker;
    import androidx.work.ListenableWorker.Result;
    import androidx.work.WorkerParameters;
    import com.google.common.util.concurrent.ListenableFuture;

    class MyWorker extends ListenableWorker {

      public MyWorker(@NonNull Context appContext, @NonNull WorkerParameters params) {
        super(appContext, params);
      }

      @Override
      public ListenableFuture<ListenableWorker.Result> startWork() {
        // Do your work here.
        Data input = getInputData();

        // Return a ListenableFuture<>
      }

      @Override
      public void onStopped() {
        // Cleanup because you are being stopped.
      }
    }
    

WorkManager の基本の処理単位は ListenableWorker です。JobService.onStartJob() と同じように、startWork() はメインスレッドで呼び出されます。ここで、MyWorkerListenableWorker を実装して ListenableFuture のインスタンスを返します。このインスタンスは処理の完了を非同期に通知するために使用されます。 ここで独自のスレッド戦略を選択する必要があります。

ここで ListenableFuture は最終的に ListenableWorker.Result 型を返します。具体的には、Result.success()Result.success(Data outputData)Result.retry()Result.failure()Result.failure(Data outputData) のいずれかです。詳細については、ListenableWorker.Result のリファレンス ページをご覧ください。

onStopped() は、条件が満たされなくなったか(ネットワークが利用できなくなったなど)、あるいは WorkManager.cancel…() メソッドが呼び出されたかのいずれかの理由で ListenableWorker が停止する必要があることを通知するために呼び出されます。また、onStopped() は、OS がなんらかの理由で処理の終了を決定した場合にも呼び出されます。

SimpleJobService を Worker に変換する

SimpleJobService を使用している場合、上記のワーカーは次のようになります。

Kotlin

    import android.content.Context;
    import androidx.work.Data;
    import androidx.work.ListenableWorker.Result;
    import androidx.work.Worker;
    import androidx.work.WorkerParameters;

    class MyWorker(context: Context, params: WorkerParameters) : Worker(context, params) {
        override fun doWork(): Result {
            TODO("Return a Result")
        }

        override fun onStopped() {
            super.onStopped()
            TODO("Cleanup, because you are being stopped")
        }
    }
    

Java

    import android.content.Context;
    import androidx.work.Data;
    import androidx.work.ListenableWorker.Result;
    import androidx.work.Worker;
    import androidx.work.WorkerParameters;

    class MyWorker extends Worker {

      public MyWorker(@NonNull Context appContext, @NonNull WorkerParameters params) {
        super(appContext, params);
      }

      @Override
      public ListenableWorker.Result doWork() {
        // Do your work here.
        Data input = getInputData();

        // Return a ListenableWorker.Result
        Data outputData = new Data.Builder()
            .putString(“Key”, “value”)
            .build();
        return Result.success(outputData);
      }

      @Override
      public void onStopped() {
        // Cleanup because you are being stopped.
      }
    }
    

ここで doWork() は、ListenableWorker.Result のインスタンスを同期的に返して、処理の完了を通知します。これは、ジョブをバックグラウンド スレッドにスケジューリングする SimpleJobService に似ています。

JobBuilder を WorkRequests に変換する

FirebaseJobBuilder は Job.Builder を使用して Job メタデータを表現します。WorkManager は、この役目を果たすために WorkRequest を使用します。

WorkManager には、OneTimeWorkRequestPeriodicWorkRequest の 2 種類の WorkRequest があります。

現在 Job.Builder.setRecurring(true) を使用している場合、新しい PeriodicWorkRequest を作成する必要があります。それ以外の場合は、OneTimeWorkRequest を使用する必要があります。

以下で FirebaseJobDispatcher を使った複雑な Job のスケジューリングの例を説明します。

Kotlin

    val input:Bundle = Bundle().apply {
        putString("some_key", "some_value")
    }

    val job = dispatcher.newJobBuilder()
        // the JobService that will be called
        .setService(MyService::class.java)
        // uniquely identifies the job
        .setTag("my-unique-tag")
        // one-off job
        .setRecurring(false)
        // don't persist past a device reboot
        .setLifetime(Lifetime.UNTIL_NEXT_BOOT)
        // start between 0 and 60 seconds from now
        .setTrigger(Trigger.executionWindow(0, 60))
        // don't overwrite an existing job with the same tag
        .setReplaceCurrent(false)
        // retry with exponential backoff
        .setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL)

        .setConstraints(
            // only run on an unmetered network
            Constraint.ON_UNMETERED_NETWORK,
            // // only run when the device is charging
            Constraint.DEVICE_CHARGING
        )
        .setExtras(input)
        .build()

    dispatcher.mustSchedule(job)
    

Java

    Bundle input = new Bundle();
    input.putString("some_key", "some_value");

    Job myJob = dispatcher.newJobBuilder()
        // the JobService that will be called
        .setService(MyJobService.class)
        // uniquely identifies the job
        .setTag("my-unique-tag")
        // one-off job
        .setRecurring(false)
        // don't persist past a device reboot
        .setLifetime(Lifetime.UNTIL_NEXT_BOOT)
        // start between 0 and 60 seconds from now
        .setTrigger(Trigger.executionWindow(0, 60))
        // don't overwrite an existing job with the same tag
        .setReplaceCurrent(false)
        // retry with exponential backoff
        .setRetryStrategy(RetryStrategy.DEFAULT_EXPONENTIAL)
        // constraints that need to be satisfied for the job to run
        .setConstraints(
            // only run on an unmetered network
            Constraint.ON_UNMETERED_NETWORK,
            // only run when the device is charging
            Constraint.DEVICE_CHARGING
        )
        .setExtras(input)
        .build();

    dispatcher.mustSchedule(myJob);
    

WorkManager で同じことを実現するには、以下を行う必要があります。

  • Worker の入力として使用できる入力データを作成する
  • 上の FirebaseJobDispatcher で定義したものと同様の入力データと制約を使用して WorkRequest を作成する
  • WorkRequest をキューに加える

ワーカーの入力をセットアップする

FirebaseJobDispatcherBundle を使用して入力データを JobService に送信します。WorkManager は代わりに Data を使用します。そのため、以下のようになります。

Kotlin

    import androidx.work.workDataOf
    val data = workDataOf("some_key" to "some_val")
    

Java

    import androidx.work.Data;
    Data input = new Data.Builder()
        .putString("some_key", "some_value")
        .build();
    

ワーカーに Constraints をセットアップする

FirebaseJobDispatcherJob.Builder.setConstaints(...) を使用してジョブに制約をセットアップします。WorkManager は代わりに Constraints を使用します。

Kotlin

    import androidx.work.*

    val constraints: Constraints = Constraints.Builder().apply {
        setRequiredNetworkType(NetworkType.CONNECTED)
        setRequiresCharging(true)
    }.build()
    

Java

    import androidx.work.Constraints;
    import androidx.work.Constraints.Builder;
    import androidx.work.NetworkType;

    Constraints constraints = new Constraints.Builder()
        // The Worker needs Network connectivity
        .setRequiredNetworkType(NetworkType.CONNECTED)
        // Needs the device to be charging
        .setRequiresCharging(true)
        .build();
    

WorkRequest(OneTime または Periodic)の作成

OneTimeWorkRequestPeriodicWorkRequest を作成するには、OneTimeWorkRequest.BuilderPeriodicWorkRequest.Builder を使用する必要があります。

上記の Job に似た OneTimeWorkRequest を作成するには、以下のようにする必要があります。

Kotlin

    import androidx.work.*
    import java.util.concurrent.TimeUnit

    val constraints: Constraints = TODO("Define constraints as above")
    val request: OneTimeWorkRequest =
         // Tell which work to execute
         OneTimeWorkRequestBuilder<MyWorker>()
             // Sets the input data for the ListenableWorker
            .setInputData(input)
            // If you want to delay the start of work by 60 seconds
            .setInitialDelay(60, TimeUnit.SECONDS)
            // Set a backoff criteria to be used when retry-ing
            .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30000, TimeUnit.MILLISECONDS)
            // Set additional constraints
            .setConstraints(constraints)
            .build()
    

Java

    import androidx.work.BackoffCriteria;
    import androidx.work.Constraints;
    import androidx.work.Constraints.Builder;
    import androidx.work.NetworkType;
    import androidx.work.OneTimeWorkRequest;
    import androidx.work.OneTimeWorkRequest.Builder;
    import androidx.work.Data;

    // Define constraints (as above)
    Constraints constraints = ...
    OneTimeWorkRequest request =
        // Tell which work to execute
        new OneTimeWorkRequest.Builder(MyWorker.class)
            // Sets the input data for the ListenableWorker
            .setInputData(inputData)
            // If you want to delay the start of work by 60 seconds
            .setInitialDelay(60, TimeUnit.SECONDS)
            // Set a backoff criteria to be used when retry-ing
            .setBackoffCriteria(BackoffCriteria.EXPONENTIAL, 30000, TimeUnit.MILLISECONDS)
            // Set additional constraints
            .setConstraints(constraints)
            .build();
    

ここでの主な違いは、WorkManager のジョブはデバイスの再起動後も常に自動的に保存されることです。

PeriodicWorkRequest を作成する場合は、以下のようにします。

Kotlin

    val constraints: Constraints = TODO("Define constraints as above")
    val request: PeriodicWorkRequest =
    PeriodicWorkRequestBuilder<MyWorker>(15, TimeUnit.MINUTES)
        // Sets the input data for the ListenableWorker
        .setInputData(input)
        // Other setters
        .build()
    

Java

    import androidx.work.BackoffCriteria;
    import androidx.work.Constraints;
    import androidx.work.Constraints.Builder;
    import androidx.work.NetworkType;
    import androidx.work.PeriodicWorkRequest;
    import androidx.work.PeriodicWorkRequest.Builder;
    import androidx.work.Data;

    // Define constraints (as above)
    Constraints constraints = ...

    PeriodicWorkRequest request =
        // Executes MyWorker every 15 minutes
        new PeriodicWorkRequest.Builder(MyWorker.class, 15, TimeUnit.MINUTES)
            // Sets the input data for the ListenableWorker
            .setInputData(input)
            . // other setters (as above)
            .build();
    

処理のスケジューリング

WorkerWorkRequest を定義したので、処理をスケジューリングする準備ができました。

FirebaseJobDispatcher で定義されたすべての Job には、Job の一意な識別に使用される tag があります。また、アプリケーションが setReplaceCurrent を呼び出して、Job のこのインスタンスが Job の既存のコピーを置き換えるかどうかをスケジューラに通知する方法を提供しています。

Kotlin

    val job = dispatcher.newJobBuilder()
        // the JobService that will be called
        .setService(MyService::class.java)
        // uniquely identifies the job
        .setTag("my-unique-tag")
        // don't overwrite an existing job with the same tag
        .setRecurring(false)
        // Other setters...
        .build()
    

Java

    Job myJob = dispatcher.newJobBuilder()
        // the JobService that will be called
        .setService(MyJobService.class)
        // uniquely identifies the job
        .setTag("my-unique-tag")
        // don't overwrite an existing job with the same tag
        .setReplaceCurrent(false)
        // other setters
        // ...

    dispatcher.mustSchedule(myJob);
    

WorkManager を使用する場合、enqueueUniqueWork()enqueueUniquePeriodicWork() の API(それぞれ、OneTimeWorkRequestPeriodicWorkRequest を使用する場合)を使用して同じ結果を得られます。詳細については、WorkManager.enqueueUniqueWork()WorkManager.enqueueUniquePeriodicWork() のリファレンス ページをご覧ください。

これは次のようになります。

Kotlin

    import androidx.work.*

    val request: OneTimeWorkRequest = TODO("A WorkRequest")
    WorkManager.getInstance(myContext)
        .enqueueUniqueWork("my-unique-name", ExistingWorkPolicy.KEEP, request)
    

Java

    import androidx.work.ExistingWorkPolicy;
    import androidx.work.OneTimeWorkRequest;
    import androidx.work.WorkManager;

    OneTimeWorkRequest workRequest = // a WorkRequest;
    WorkManager.getInstance(myContext)
        // Use ExistingWorkPolicy.REPLACE to cancel and delete any existing pending
        // (uncompleted) work with the same unique name. Then, insert the newly-specified
        // work.
        .enqueueUniqueWork("my-unique-name", ExistingWorkPolicy.KEEP, workRequest);
    

処理のキャンセル

FirebaseJobDispatcher では、以下を使用して処理をキャンセルしました。

Kotlin

    dispatcher.cancel("my-unique-tag")
    

Java

    dispatcher.cancel("my-unique-tag");
    

WorkManager を使用する場合は、以下を使用します。

Kotlin

    import androidx.work.WorkManager
    WorkManager.getInstance(myContext).cancelUniqueWork("my-unique-name")
    

Java

    import androidx.work.WorkManager;
    WorkManager.getInstance(myContext).cancelUniqueWork("my-unique-name");
    

WorkManager の初期化

WorkManager は、アプリごとに 1 回初期化する必要があります。これには通常、ContentProvider または Application.onCreate() を使用します。

WorkManager は通常、ContentProvider を使用して自身を初期化します。ただし、スレッドプールのサイズと、特定の時間にスケジュールできるワーカーの数に関して、デフォルトに微妙な違いがいくつかあります。そのため、WorkManager のカスタマイズを必要とする場合があります。

通常、このカスタマイズには WorkManager.initialize() を使用します。これにより、Worker の実行に使用されるバックグラウンド Executor と、Workers の作成に使用される WorkerFactory のカスタマイズができます。(WorkerFactory は、依存性注入の状況で役に立ちます)。WorkManager の自動的な初期化を止める方法については、このメソッドのドキュメントをご覧ください。

詳細については、initialize()Configuration.Builder のドキュメントをご覧ください。