Migrer de Firebase JobDispatcher vers WorkManager

WorkManager est une bibliothèque permettant de planifier et d'exécuter des tâches différables en arrière-plan dans Android. Il s'agit du remplacement recommandé pour Firebase JobDispatcher. Le guide suivant vous guide tout au long du processus de migration de votre implémentation Firebase JobDispatcher vers WorkManager.

Configuration de Gradle

Pour importer la bibliothèque WorkManager dans votre projet Android, ajoutez les dépendances listées dans Premiers pas avec WorkManager.

De JobService aux nœuds de calcul

FirebaseJobDispatcher utilise une sous-classe de JobService comme point d'entrée pour définir la tâche à effectuer. Vous utilisez peut-être directement JobService ou SimpleJobService.

Un JobService se présente comme suit :

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?"
    }
}

Si vous utilisez SimpleJobService, vous aurez remplacé onRunJob(), qui renvoie un type @JobResult int.

La différence principale est que lorsque vous utilisez directement JobService, onStartJob() est appelé sur le thread principal. Il est de la responsabilité de l'application de décharger le travail dans un thread en arrière-plan. En revanche, si vous utilisez SimpleJobService, ce service est chargé d'exécuter votre travail sur un thread d'arrière-plan.

WorkManager présente des concepts similaires. L'unité de travail fondamentale dans WorkManager est un ListenableWorker. Il existe également d'autres sous-types utiles de nœuds de calcul, tels que Worker, RxWorker et CoroutineWorker (si vous utilisez des coroutines Kotlin).

JobService est mappé à un ListenableWorker

Si vous utilisez directement JobService, le nœud de calcul auquel il est mappé est ListenableWorker. Si vous utilisez SimpleJobService, utilisez plutôt Worker.

Utilisons l'exemple ci-dessus (MyJobService) et voyons comment le convertir en 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.
  }
}

L'unité de travail de base dans WorkManager est ListenableWorker. Tout comme JobService.onStartJob(), startWork() est appelé sur le thread principal. Ici MyWorker implémente ListenableWorker et renvoie une instance de ListenableFuture, qui permet de signaler que la tâche est terminée de manière asynchrone. Vous devez choisir votre propre stratégie d'exécution de threads.

ListenableFuture ici renvoie un type ListenableWorker.Result qui peut être Result.success(), Result.success(Data outputData), Result.retry(), Result.failure() ou Result.failure(Data outputData). Pour en savoir plus, consultez la page de référence de ListenableWorker.Result.

onStopped() est appelé pour indiquer que ListenableWorker doit s'arrêter, soit parce que les contraintes ne sont plus respectées (par exemple, parce que le réseau n'est plus disponible), soit parce qu'une méthode WorkManager.cancel…() a été appelée. onStopped() peut également être appelé si le système d'exploitation décide d'arrêter votre tâche pour une raison quelconque.

SimpleJobService est mappé à un nœud de calcul

Lorsque vous utilisez SimpleJobService, le nœud de calcul ci-dessus se présente comme suit :

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 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.
  }
}

Ici, doWork() renvoie une instance de ListenableWorker.Result pour signaler la fin d'une tâche de manière synchrone. Cette méthode est semblable à SimpleJobService, qui programme les tâches sur un thread en arrière-plan.

JobBuilder est mappé à WorkRequest

FirebaseJobBuilder utilise Job.Builder pour représenter les métadonnées Job. WorkManager utilise WorkRequest pour remplir ce rôle.

WorkManager comporte deux types de WorkRequest : OneTimeWorkRequest et PeriodicWorkRequest.

Si vous utilisez actuellement Job.Builder.setRecurring(true), vous devez créer un objet PeriodicWorkRequest. Sinon, utilisez un objet OneTimeWorkRequest.

Voyons à quoi pourrait ressembler la planification d'un Job complexe avec FirebaseJobDispatcher :

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);

Pour obtenir le même résultat avec WorkManager, procédez comme suit :

  • Créez des données d'entrée pouvant être utilisées comme entrées pour le Worker.
  • Créez une WorkRequest avec des données et contraintes d'entrée semblables à celles définies ci-dessus pour FirebaseJobDispatcher.
  • Mettez WorkRequest en file d'attente.

Configurer des entrées pour le nœud de calcul

FirebaseJobDispatcher utilise un Bundle pour envoyer les données d'entrée au JobService. WorkManager utilise Data à la place. Vous obtenez ainsi :

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();

Configurer des contraintes pour le nœud de calcul

FirebaseJobDispatcher utilise Job.Builder.setConstaints(...) pour configurer des contraintes sur les tâches. WorkManager utilise Constraints à la place.

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();

Créer la WorkRequest (à usage ponctuel ou périodique)

Pour créer des éléments OneTimeWorkRequest et PeriodicWorkRequest, vous devez utiliser OneTimeWorkRequest.Builder et PeriodicWorkRequest.Builder.

Pour créer une OneTimeWorkRequest semblable au Job ci-dessus, procédez comme suit :

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();

La principale différence réside dans le fait que les tâches de WorkManager sont toujours conservées automatiquement lors du redémarrage de l'appareil.

Si vous souhaitez créer une PeriodicWorkRequest, vous devez procéder comme suit :

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();

Planifier une tâche

Maintenant que vous avez défini Worker et WorkRequest, vous êtes prêt à planifier une tâche.

Chaque Job défini avec FirebaseJobDispatcher avait un tag qui était utilisé pour identifier de manière unique un Job. Cela permettait également à l'application d'indiquer au programmeur si cette instance de Job doit remplacer une copie existante de Job en appelant setReplaceCurrent.

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);

Lorsque vous utilisez WorkManager, vous pouvez obtenir le même résultat en utilisant les API enqueueUniqueWork() et enqueueUniquePeriodicWork() (lorsque vous utilisez respectivement OneTimeWorkRequest et PeriodicWorkRequest). Pour en savoir plus, consultez les pages de référence sur WorkManager.enqueueUniqueWork() et WorkManager.enqueueUniquePeriodicWork().

Le résultat devrait ressembler à ceci :

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);

Annuler une tâche

Avec FirebaseJobDispatcher, vous pouvez annuler votre tâche avec :

Kotlin

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

Java

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

Lorsque vous utilisez WorkManager, vous pouvez utiliser :

Kotlin

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

Java

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

Initialiser WorkManager

WorkManager s'initialise généralement à l'aide d'un ContentProvider. Si vous avez besoin de mieux contrôler la façon dont WorkManager organise et programme les tâches, vous pouvez personnaliser la configuration et l'initialisation de WorkManager.