Cómo migrar de Firebase JobDispatcher a WorkManager

WorkManager es una biblioteca para programar y ejecutar trabajos en segundo plano diferibles en Android. Es el reemplazo recomendado de Firebase JobDispatcher. La siguiente guía te indicará cómo realizar el proceso de migración de tu implementación de Firebase JobDispatcher a WorkManager.

Configuración de Gradle

De JobService a Workers

FirebaseJobDispatcher usa una subclase de JobService como punto de entrada para definir el trabajo que se debe llevar a cabo. Puedes usar JobService directamente o SimpleJobService.

Un JobService tendrá un aspecto similar al siguiente:

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 estás usando SimpleJobService, entonces anulaste onRunJob(), que muestra un tipo @JobResult int.

La diferencia clave es cuando usas JobService de forma directa, se llama a onStartJob() en el subproceso principal y es responsabilidad de la app descargar el trabajo en un subproceso en segundo plano. Por otra parte, si usas SimpleJobService, ese servicio es responsable de ejecutar tu trabajo en un subproceso en segundo plano.

WorkManager tiene conceptos similares. La unidad de trabajo fundamental de WorkManager es ListenableWorker. También hay otros subtipos de trabajadores útiles, como Worker, RxWorker y CoroutineWorker (cuando se usan corrutinas de Kotlin).

Asignaciones de JobService a ListenableWorker

Si estás usando JobService de forma directa, este mapea al trabajador ListenableWorker. Si usas SimpleJobService, deberás cambiar a Worker.

Usemos el ejemplo anterior (MyJobService) y veamos cómo podemos convertirlo 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.
      }
    }
    

La unidad básica de trabajo en WorkManager es ListenableWorker. Al igual que con JobService.onStartJob(), se llama a startWork() en el subproceso principal. Aquí, MyWorker implementa ListenableWorker y muestra una instancia de ListenableFuture, que se usa para indicar la finalización del trabajo de forma asíncrona. Debes elegir tu propia estrategia de subprocesos aquí.

ListenableFuture aquí eventualmente muestra el tipo ListenableWorker.Result, que puede ser Result.success(), Result.success(Data outputData), Result.retry(), Result.failure() o Result.failure(Data outputData). Para obtener más información, consulta la página de referencia de ListenableWorker.Result.

Se llama a onStopped() para indicar que ListenableWorker necesita detenerse, ya sea porque ya no se cumplen las restricciones (por ejemplo, porque la red ya no está disponible) o porque se llamó a un método WorkManager.cancel…(). También se puede llamar a onStopped() si el sistema operativo decide finalizar el trabajo por algún motivo.

Asignaciones de SimpleJobService a un Worker

Cuando se usa SimpleJobService, el trabajador de arriba es similar a:

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

Aquí, doWork() muestra una instancia de ListenableWorker.Result para indicar la finalización síncrona del trabajo, de manera similar a SimpleJobService, que programa trabajos en un subproceso en segundo plano.

Asignaciones de JobBuilder a WorkRequests

FirebaseJobBuilder usa Job.Builder para representar metadatos de Job. WorkManager usa WorkRequest para cumplir esta función.

WorkManager tiene dos tipos de WorkRequest: OneTimeWorkRequest y PeriodicWorkRequest.

Si estás usando Job.Builder.setRecurring(true), debes crear una nueva instancia de PeriodicWorkRequest. De lo contrario, debes usar OneTimeWorkRequest.

Veamos cómo es programar un Job complejo con 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);
    

Para obtener el mismo resultado con WorkManager deberás hacer lo siguiente:

  • Compilar datos de entrada, que se pueden usar como entrada para Worker
  • Compilar una WorkRequest con datos de entrada y restricciones similares a los definidos arriba para FirebaseJobDispatcher
  • Colocar WorkRequest en la cola

Cómo configurar entradas para Worker

FirebaseJobDispatcher usa un Bundle para enviar datos de entrada a JobService, mientras que WorkManager usa Data. Estos son los resultados:

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

Cómo configurar restricciones para Worker

FirebaseJobDispatcher usa Job.Builder.setConstaints(...) para configurar restricciones en los trabajos. WorkManager en su lugar usa 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();
    

Cómo crear la WorkRequest (única o periódica)

Para crear OneTimeWorkRequest y PeriodicWorkRequest, debes usar OneTimeWorkRequest.Builder y PeriodicWorkRequest.Builder.

Para crear una instancia de OneTimeWorkRequest similar a la instancia de Job anterior, haz lo siguiente:

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 diferencia clave aquí es que los trabajos de WorkManager siempre persisten automáticamente en los reinicios de dispositivos.

Si quieres crear una instancia de PeriodicWorkRequest, usa el siguiente ejemplo:

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

Cómo programar trabajos

Ahora que definiste un Worker y una WorkRequest, ya puedes programar un trabajo.

Cada Job definido con FirebaseJobDispatcher tenía una tag que se usaba para identificar de forma exclusiva un Job, así como permitir que la aplicación le indique al programador si esta instancia de Job es para reemplazar una copia existente del Job llamando a 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);
    

Cuando usas WorkManager, puedes lograr el mismo resultado usando las API enqueueUniqueWork() y enqueueUniquePeriodicWork() (cuando se usa OneTimeWorkRequest y PeriodicWorkRequest, respectivamente). Para obtener más información, consulta las páginas de referencia de WorkManager.enqueueUniqueWork() y WorkManager.enqueueUniquePeriodicWork().

El código tendrá el siguiente aspecto:

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

Cómo cancelar trabajos

Con FirebaseJobDispatcher, puedes cancelar trabajos con de la siguiente manera:

Kotlin

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

Java

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

Con WorkManager, puedes usar lo siguiente:

Kotlin

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

Java

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

Cómo inicializar WorkManager

WorkManager se debe inicializar una vez por app, en general mediante una instancia de ContentProvider o Application.onCreate().

WorkManager generalmente se inicializa mediante ContentProvider. Sin embargo, hay algunas diferencias sutiles en los valores predeterminados con respecto al tamaño del conjunto de subprocesos y al número de trabajadores que se pueden programar en un momento dado. Por lo tanto, es posible que debas personalizar WorkManager.

Generalmente, esta personalización se lleva a cabo mediante WorkManager.initialize(), lo que te permite personalizar la instancia de Executor en segundo plano usada para ejecutar Worker, y la instancia de WorkerFactory usada para construir Workers. (WorkerFactory es útil en el contexto de la inyección de dependencias). Consulta la documentación de este método para asegurarte de detener la inicialización automática de WorkManager.

Para obtener más información, consulta la documentación sobre initialize() y Configuration.Builder.