Migracja z Firebase JobDispatcher do WorkManager

WorkManager to biblioteka do planowania i wykonywania odroczonej pracy w tle na Androidzie. Jest zalecanym zamiennikiem JobDispatcher w Firebase. Z tego przewodnika dowiesz się, jak przenieść implementację FirebaseJobDispatcher do WorkManagera.

Konfigurowanie Gradle

Aby zaimportować bibliotekę WorkManager do projektu Androida, dodaj zależności wymienione w artykule Pierwsze kroki z WorkManagerem.

Z JobService do instancji roboczych

Metoda FirebaseJobDispatcher używa podklasy JobService jako punktu wejścia do definiowania zadań do wykonania. Możliwe, że używasz JobService bezpośrednio lub SimpleJobService.

JobService będzie wyglądać mniej więcej tak:

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

Jeśli używasz typu SimpleJobService, zastąpisz atrybut onRunJob(), który zwraca typ @JobResult int.

Główna różnica polega na tym, że używasz metody JobService bezpośrednio, onStartJob() jest wywoływana w wątku głównym, a za przeciążenie zadania do wątku w tle odpowiada aplikacja. Z drugiej strony, jeśli używasz usługi SimpleJobService, jest ona odpowiedzialna za wykonywanie Twojej pracy w wątku w tle.

Podobne koncepcje w usłudze WorkManager. Podstawową jednostką pracy w usłudze WorkManager jest ListenableWorker. Istnieją też inne przydatne podtypy instancji roboczych, takie jak Worker, RxWorker i CoroutineWorker (podczas korzystania z kotlinów).

JobService mapuje się na ListenableWorker

Jeśli używasz bezpośrednio środowiska JobService, instancja robocza, na którą mapuje się dane, to ListenableWorker. Jeśli używasz parametru SimpleJobService, użyj zamiast niego Worker.

Wykorzystajmy powyższy przykład (MyJobService) i sprawdźmy, jak przekonwertować go na 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.
  }
}

Podstawową jednostką pracy w usłudze WorkManager jest ListenableWorker. Tak jak JobService.onStartJob(), funkcja startWork() jest wywoływana w wątku głównym. W tym miejscu MyWorker implementuje właściwość ListenableWorker i zwraca wystąpienie ListenableFuture, które służy do sygnalizowania ukończenia pracy asynchronicznie. Tu należy wybrać własną strategię podziału na wątki.

ListenableFuture w tym miejscu zwraca typ ListenableWorker.Result, który może być jednym z tych elementów: Result.success(), Result.success(Data outputData), Result.retry(), Result.failure() lub Result.failure(Data outputData). Więcej informacji znajdziesz na stronie z dokumentacją ListenableWorker.Result.

Funkcja onStopped() jest wywoływana, aby zasygnalizować konieczność zatrzymania ListenableWorker, ponieważ ograniczenia nie są już spełnione (np. sieć nie jest już dostępna) lub wywołano metodę WorkManager.cancel…(). Usługa onStopped() może też być wywołana, jeśli system operacyjny z jakiegoś powodu wyłączy Twoją pracę.

Mapy SimpleJobService na instancję roboczą

Gdy używasz SimpleJobService, powyższa instancja robocza będzie wyglądać tak:

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

W tym przypadku doWork() zwraca wystąpienie ListenableWorker.Result, aby zasygnalizować synchronicznie zakończenie zadania. Usługa działa podobnie do usługi SimpleJobService, która planuje zadania w wątku w tle.

JobBuilder mapuje na WorkRequest

FirebaseJobBuilder używa elementu Job.Builder do reprezentowania metadanych Job. WorkManager używa do tego roli WorkRequest.

WorkManager zawiera 2 typy właściwości WorkRequest: OneTimeWorkRequest i PeriodicWorkRequest.

Jeśli obecnie używasz Job.Builder.setRecurring(true), utwórz nowy PeriodicWorkRequest. W przeciwnym razie użyj właściwości OneTimeWorkRequest.

Przyjrzyjmy się, jak może wyglądać złożone harmonogramy Job z użyciem 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);

Aby osiągnąć ten sam efekt w usłudze WorkManager, musisz:

  • Tworzenie danych wejściowych, których można użyć jako danych wejściowych dla funkcji Worker.
  • Utwórz WorkRequest z danymi wejściowymi i ograniczeniami podobnymi do tych zdefiniowanych powyżej dla atrybutu FirebaseJobDispatcher.
  • Umieść WorkRequest w kolejce.

Konfigurowanie danych wejściowych dla instancji roboczej

FirebaseJobDispatcher używa Bundle do wysyłania danych wejściowych do JobService. WorkManager używa zamiast niej Data. To zmienia się w ten sposób:

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

Konfigurowanie ograniczeń dla instancji roboczej

FirebaseJobDispatcher używa Job.Builder.setConstaints(...) do konfigurowania ograniczeń zadań. WorkManager używa zamiast niej 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();

Tworzenie żądania WorkRequest (jednorazowego lub okresowego)

Do tworzenia komponentów OneTimeWorkRequest i PeriodicWorkRequest należy użyć narzędzi OneTimeWorkRequest.Builder i PeriodicWorkRequest.Builder.

Aby utworzyć OneTimeWorkRequest podobny do tego Job, wykonaj te czynności:

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

Główną różnicą jest to, że zadania WorkManagera są zawsze zachowane po ponownym uruchomieniu urządzenia.

Jeśli chcesz utworzyć PeriodicWorkRequest, możesz to zrobić na przykład:

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

Planuję pracę

Po określeniu Worker i WorkRequest możesz zaplanować pracę.

Każdy element Job zdefiniowany z zastosowaniem FirebaseJobDispatcher miał parametr tag, który służył do jednoznacznej identyfikacji elementu Job. Aplikacja może też informować algorytm szeregowania, czy ta instancja Job ma zastąpić istniejącą kopię obiektu Job, wywołując metodę 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);

Korzystając z WorkManagera, możesz osiągnąć ten sam efekt za pomocą interfejsów API enqueueUniqueWork() i enqueueUniquePeriodicWork() (odpowiednio OneTimeWorkRequest i PeriodicWorkRequest). Więcej informacji znajdziesz na stronach z informacjami o WorkManager.enqueueUniqueWork() i WorkManager.enqueueUniquePeriodicWork().

Będzie to wyglądać mniej więcej tak:

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

Anuluję pracę

W przypadku usługi FirebaseJobDispatcher możesz anulować zadanie za pomocą:

Kotlin

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

Java

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

Podczas korzystania z WorkManagera możesz używać tych funkcji:

Kotlin

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

Java

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

Inicjuję usługę WorkManager

WorkManager zwykle inicjuje się przy użyciu ContentProvider. Jeśli potrzebujesz większej kontroli nad sposobem, w jaki WorkManager porządkuje i działa harmonogramy, możesz dostosować konfigurację i inicjowanie WorkManagera.