Cómo ejecutar subprocesos en ListenableWorker

En ciertas situaciones, es posible que debas proporcionar una estrategia de subprocesos personalizada. Por ejemplo, es posible que debas controlar una operación asíncrona basada en devolución de llamada. WorkManager admite este caso de uso con ListenableWorker. ListenableWorker es la API de trabajador más básica. Worker, CoroutineWorker y RxWorker derivan de esta clase. Un ListenableWorker solo señala cuándo debe comenzar y detenerse el trabajo, y te deja la tarea completamente a ti. La señal de inicio de trabajo se invoca en el subproceso principal, por lo que es muy importante que vayas a un subproceso en segundo plano que elijas de forma manual.

El método abstracto ListenableWorker.startWork() muestra un ListenableFuture de la Result. Un ListenableFuture es una interfaz liviana: es una Future que proporciona funcionalidad para adjuntar objetos de escucha y propagar excepciones. En el método startWork, se espera que muestres un ListenableFuture, que establecerás con el Result de la operación una vez que se complete. Puedes crear instancias de ListenableFuture de dos maneras:

  1. Si usas Guava, utiliza ListeningExecutorService.
  2. De lo contrario, incluye councurrent-futures en tu archivo de Gradle y usa CallbackToFutureAdapter.

Si quisieras ejecutar un trabajo basado en una devolución de llamada asíncrona, harías algo como lo siguiente:

Kotlin

class CallbackWorker(
        context: Context,
        params: WorkerParameters
) : ListenableWorker(context, params) {
    override fun startWork(): ListenableFuture<Result> {
        return CallbackToFutureAdapter.getFuture { completer ->
            val callback = object : Callback {
                var successes = 0

                override fun onFailure(call: Call, e: IOException) {
                    completer.setException(e)
                }

                override fun onResponse(call: Call, response: Response) {
                    successes++
                    if (successes == 100) {
                        completer.set(Result.success())
                    }
                }
            }

            repeat(100) {
                downloadAsynchronously("https://example.com", callback)
            }

            callback
        }
    }
}

Java

public class CallbackWorker extends ListenableWorker {

    public CallbackWorker(Context context, WorkerParameters params) {
        super(context, params);
    }

    @NonNull
    @Override
    public ListenableFuture<Result> startWork() {
        return CallbackToFutureAdapter.getFuture(completer -> {
            Callback callback = new Callback() {
                int successes = 0;

                @Override
                public void onFailure(Call call, IOException e) {
                    completer.setException(e);
                }

                @Override
                public void onResponse(Call call, Response response) {
                    successes++;
                    if (successes == 100) {
                        completer.set(Result.success());
                    }
                }
            };

            for (int i = 0; i < 100; i++) {
                downloadAsynchronously("https://www.example.com", callback);
            }
            return callback;
        });
    }
}

¿Qué sucede si tu trabajo se detiene? Un elemento ListenableFuture de ListenableWorker siempre se cancela cuando se espera que se detenga el trabajo. Con un CallbackToFutureAdapter, solo tienes que agregar un objeto de escucha de cancelación, de la siguiente manera:

Kotlin

class CallbackWorker(
        context: Context,
        params: WorkerParameters
) : ListenableWorker(context, params) {
    override fun startWork(): ListenableFuture<Result> {
        return CallbackToFutureAdapter.getFuture { completer ->
            val callback = object : Callback {
                var successes = 0

                override fun onFailure(call: Call, e: IOException) {
                    completer.setException(e)
                }

                override fun onResponse(call: Call, response: Response) {
                    ++successes
                    if (successes == 100) {
                        completer.set(Result.success())
                    }
                }
            }

 completer.addCancellationListener(cancelDownloadsRunnable, executor)

            repeat(100) {
                downloadAsynchronously("https://example.com", callback)
            }

            callback
        }
    }
}

Java

public class CallbackWorker extends ListenableWorker {

    public CallbackWorker(Context context, WorkerParameters params) {
        super(context, params);
    }

    @NonNull
    @Override
    public ListenableFuture<Result> startWork() {
        return CallbackToFutureAdapter.getFuture(completer -> {
            Callback callback = new Callback() {
                int successes = 0;

                @Override
                public void onFailure(Call call, IOException e) {
                    completer.setException(e);
                }

                @Override
                public void onResponse(Call call, Response response) {
                    ++successes;
                    if (successes == 100) {
                        completer.set(Result.success());
                    }
                }
            };

            completer.addCancellationListener(cancelDownloadsRunnable, executor);

            for (int i = 0; i < 100; ++i) {
                downloadAsynchronously("https://www.example.com", callback);
            }
            return callback;
        });
    }
}

Cómo ejecutar un ListenableWorker en un proceso diferente

También puedes vincular un trabajador a un proceso específico mediante RemoteListenableWorker, que es una implementación de ListenableWorker.

RemoteListenableWorker vincula un proceso específico con dos argumentos adicionales que puedes proporcionar como parte de los datos de entrada cuando compilas la solicitud de trabajo: ARGUMENT_CLASS_NAME y ARGUMENT_PACKAGE_NAME.

En el siguiente ejemplo, se muestra cómo compilar una solicitud de trabajo vinculada a un proceso específico:

Kotlin

val PACKAGE_NAME = "com.example.background.multiprocess"

val serviceName = RemoteWorkerService::class.java.name
val componentName = ComponentName(PACKAGE_NAME, serviceName)

val data: Data = Data.Builder()
   .putString(ARGUMENT_PACKAGE_NAME, componentName.packageName)
   .putString(ARGUMENT_CLASS_NAME, componentName.className)
   .build()

return OneTimeWorkRequest.Builder(ExampleRemoteListenableWorker::class.java)
   .setInputData(data)
   .build()

Java

String PACKAGE_NAME = "com.example.background.multiprocess";

String serviceName = RemoteWorkerService.class.getName();
ComponentName componentName = new ComponentName(PACKAGE_NAME, serviceName);

Data data = new Data.Builder()
        .putString(ARGUMENT_PACKAGE_NAME, componentName.getPackageName())
        .putString(ARGUMENT_CLASS_NAME, componentName.getClassName())
        .build();

return new OneTimeWorkRequest.Builder(ExampleRemoteListenableWorker.class)
        .setInputData(data)
        .build();

Para cada RemoteWorkerService, también debes agregar una definición del servicio en el archivo AndroidManifest.xml:

<manifest ... >
    <service
            android:name="androidx.work.multiprocess.RemoteWorkerService"
            android:exported="false"
            android:process=":worker1" />

        <service
            android:name=".RemoteWorkerService2"
            android:exported="false"
            android:process=":worker2" />
    ...
</manifest>

Ejemplos