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. En la siguiente guía, descubrirás cómo realizar el proceso de migración de tu implementación de Firebase JobDispatcher a WorkManager.

Configuración de Gradle

Para importar la biblioteca de WorkManager a tu proyecto de Android, agrega las dependencias enumeradas en Cómo comenzar a usar WorkManager.

De JobService a los trabajadores

FirebaseJobDispatcher usa una subclase de JobService como punto de entrada para definir el trabajo que se debe llevar a cabo. Puedes usar directamente JobService 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 que cuando usas directamente JobService, 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 directamente JobService, 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í.

Eventualmente, ListenableFuture muestra un 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 debe 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 SO decide cerrar el trabajo por algún motivo.

Asignaciones de SimpleJobService a un trabajador

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 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 WorkRequest

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 actualmente usas Job.Builder.setRecurring(true), deberías crear una PeriodicWorkRequest nueva. De lo contrario, deberías usar una 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 el WorkRequest en la cola

Cómo configurar entradas para Worker

FirebaseJobDispatcher usa un Bundle para enviar datos de entrada a JobService. En cambio, 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. En cambio, WorkManager 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 instancias de OneTimeWorkRequest y PeriodicWorkRequest, deberías usar OneTimeWorkRequest.Builder y PeriodicWorkRequest.Builder.

Para crear una instancia OneTimeWorkRequest que sea 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, además de permitir que la aplicación le indicara al programador si esta instancia de Job era 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 de enqueueUniqueWork() y enqueueUniquePeriodicWork() (cuando se usan 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 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 suele inicializarse mediante un objeto ContentProvider. Si quieres obtener más control sobre la manera en que WorkManager organiza y programa el trabajo, puedes personalizar los ajustes y la inicialización de WorkManager.