Trabalho em segundo plano com a WorkManager (Java)

Existem muitas opções no Android para trabalhos em segundo plano adiáveis. Este codelab abrange a WorkManager, uma biblioteca compatível, flexível e simples para trabalhos em segundo plano adiáveis. A WorkManager é a programadora de tarefas recomendada para usar no Android para trabalhos adiáveis, com garantia de execução.

O que é a WorkManager?

A WorkManager faz parte do Android Jetpack e de um componente de arquitetura para trabalho em segundo plano que requer uma combinação de execução oportunista e garantida. Na execução oportunista, a WorkManager fará o trabalho em segundo plano o quanto antes. Na execução garantida, ela cuidará da lógica para iniciar o trabalho em diversas situações, mesmo se você sair do app.

A WorkManager é uma biblioteca simples, mas incrivelmente flexível, que oferece diversos outros benefícios. São eles:

  • Compatibilidade com tarefas únicas e periódicas assíncronas
  • Compatibilidade com restrições, como condições de rede, espaço de armazenamento e status de carregamento
  • Encadeamento de solicitações de trabalho complexas, incluindo a execução de trabalhos em paralelo
  • Saída de uma solicitação de trabalho usada como entrada para a próxima
  • Gerencia a compatibilidade de nível da API de volta até o nível 14. Consulte a observação
  • Funciona com ou sem o Google Play Services
  • Segue as práticas recomendadas de integridade do sistema
  • Compatibilidade do LiveData para exibir o estado da solicitação de trabalho de forma simples na IU

Quando usar a WorkManager?

A biblioteca WorkManager é uma boa opção para tarefas que oferecem uma conclusão útil, mesmo que o usuário saia de uma tela específica do seu app.

Alguns exemplos de tarefas que fazem um bom uso da WorkManager:

  • Upload de registros
  • Aplicação de filtros a imagens e salvamento da imagem
  • Sincronização periódica de dados locais com a rede

A WorkManager oferece execução garantida, mas nem todas as tarefas exigem isso. Por isso, ela não é uma exigência para executar todas as tarefas da linha de execução principal. Para saber mais sobre quando usar a WorkManager, confira o Guia para o processamento em segundo plano.

O que você criará

Atualmente, os smartphones são muito bons para tirar fotos. Os dias em que um fotógrafo conseguia tirar uma foto desfocada de algo misterioso ficaram no passado.

Neste codelab, você trabalhará no Blur-O-Matic, um app que desfoca fotos e imagens e salva o resultado em um arquivo. Aquilo era o monstro do Lago Ness ou um submarino de brinquedo? (em inglês). Com o Blur-O-Matic, ninguém jamais saberá.

Foto de um robalo-riscado de Peggy Greb, Serviço de Pesquisa Agrícola do Departamento Agrícola dos EUA (USDA, na sigla em inglês).

O que você aprenderá

  • Como adicionar a WorkManager ao seu projeto
  • Como programar uma tarefa simples
  • Parâmetros de entrada e saída
  • Como fazer o encadeamento de trabalhos
  • Trabalhos únicos
  • Como exibir o status de trabalho na IU
  • Como cancelar trabalhos
  • Restrições de trabalho

Pré-requisitos

Se você não entender algum ponto…

Se você não entender algum ponto deste codelab ou quiser olhar o estado final do código, use o link a seguir:

Download do código final

Ou, se preferir, você pode clonar o codelab concluído da WorkManager no GitHub:

$ git clone -b java https://github.com/googlecodelabs/android-workmanager

Etapa 1: fazer o download do código

Clique no link abaixo para fazer o download de todo o código para este codelab:

Download do código inicial

Ou, se preferir, clone o codelab de navegação no GitHub:

$ git clone -b start_java https://github.com/googlecodelabs/android-workmanager

Etapa 2: obter uma imagem

Se você está usando um dispositivo no qual já fez o download de imagens ou tirou fotos, podemos começar.

Se está usando um dispositivo novo (como um emulador recém-criado), tire uma foto ou faça o download de uma imagem na Web com ele. Escolha algo misterioso.

Etapa 3: executar o app

Execute o app. As telas a seguir serão exibidas. Conceda as permissões de acesso às fotos na solicitação inicial e, se a imagem for desativada, reabra o app:

Você pode selecionar uma imagem e avançar para a próxima tela, que tem botões de opção para selecionar o nível de desfoque que você quer aplicar na imagem. Quando o botão Go for pressionado, a imagem será desfocada e salva.

Por enquanto, o app não aplica nenhum desfoque.

O código inicial contém o seguinte:

  • ****WorkerUtils: essa classe contém o código para desfoque e alguns métodos práticos que você usará posteriormente para exibir Notifications e deixar o app mais lento.
  • *****BlurActivity: a atividade que mostra a imagem e inclui botões de opção para selecionar o nível de desfoque.
  • *****BlurViewModel: esse modelo de visualização armazena todos os dados necessários para exibir a BlurActivity. Ele também será a classe em que você iniciará o trabalho em segundo plano usando a WorkManager.
  • ****Constants: uma classe estática com algumas constantes que serão usadas durante o codelab.
  • ****SelectImageActivity: a primeira atividade que permite selecionar uma imagem.
  • res/activity_blur.xml e res/activity_select.xml: os arquivos de layout de cada atividade.

***** Esses são os únicos arquivos em que você programará códigos.

A WorkManager requer a dependência do Gradle abaixo. Ela já foi incluída nos arquivos de compilação:

app/build.gradle

dependencies {
    // Other dependencies
    implementation "androidx.work:work-runtime:$versions.work"
}

Clique aqui para encontrar a versão mais recente disponível de work-runtime e inclua a versão correta. No momento, a versão mais recente é:

build.gradle

versions.work = "2.3.3"

Se você atualizar sua versão para uma mais recente, use Sync Now para sincronizar seu projeto com os arquivos do Gradle alterados.

Nesta etapa, você usará uma imagem na pasta res/drawable chamada test.jpg e executará algumas funções nela em segundo plano. Essas funções desfocam a imagem e a salvam em um arquivo temporário.

Noções básicas da WorkManager

Há algumas classes da WorkManager que você precisa conhecer:

  • Worker: é nessa classe que você coloca o código do trabalho que quer realizar em segundo plano. Você ampliará essa classe e substituirá o método doWork().
  • WorkRequest: representa uma solicitação para realizar algum trabalho. Você transmitirá o Worker como parte da criação da WorkRequest. Ao criar a WorkRequest, você também pode especificar itens como Constraints ou quando o Worker deve ser executado.
  • WorkManager: essa classe programa suas WorkRequest e as executa. Ela programa WorkRequests de modo a distribuir a carga nos recursos do sistema, respeitando as restrições especificadas.

No seu caso, você definirá um novo BlurWorker, que conterá o código para desfocar uma imagem. Quando o botão Go for clicado, uma WorkRequest será criada e colocada na fila pela WorkManager.

Etapa 1: criar a BlurWorker

No pacote workers, crie uma nova classe chamada BlurWorker.

Ela precisa ampliar o Worker.

Etapa 2: adicionar um construtor

Adicione um construtor à classe BlurWorker:

public class BlurWorker extends Worker {
    public BlurWorker(
        @NonNull Context appContext,
        @NonNull WorkerParameters workerParams) {
            super(appContext, workerParams);
    }
}

Etapa 3: substituir e implementar doWork()

O Worker desfocará a imagem res/test.jpg.

Substitua o método doWork() e implemente o seguinte:

  1. Acesse um Context chamando getApplicationContext(). Ele será necessário para várias manipulações de bitmap que você está prestes a fazer.
  2. Crie um Bitmap com a imagem de teste:
Bitmap picture = BitmapFactory.decodeResource(
    applicationContext.getResources(),
    R.drawable.test);
  1. Acesse uma versão desfocada do bitmap chamando o método estático blurBitmap de WorkerUtils.
  2. Grave esse bitmap em um arquivo temporário chamando o método estático writeBitmapToFile de WorkerUtils. Salve o URI retornado em uma variável local.
  3. Faça uma notificação exibir o URI chamando o método estático makeStatusNotification de WorkerUtils.
  4. Retorne Result.success();.
  5. Una o código das etapas 2 a 6 em uma instrução try/catch. Capture um Throwable genérico.
  6. Na declaração de captura, emita um erro log statement: Log.e(TAG, "Error applying blur", throwable);.
  7. Na declaração de captura, retorne Result.failure();.

O código completo desta etapa é mostrado abaixo.

BlurWorker.java

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.util.Log;

import com.example.background.R;

import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;

public class BlurWorker extends Worker {
    public BlurWorker(
            @NonNull Context appContext,
            @NonNull WorkerParameters workerParams) {
        super(appContext, workerParams);
    }

    private static final String TAG = BlurWorker.class.getSimpleName();

    @NonNull
    @Override
    public Result doWork() {

        Context applicationContext = getApplicationContext();

        try {

            Bitmap picture = BitmapFactory.decodeResource(
                    applicationContext.getResources(),
                    R.drawable.test);

            // Blur the bitmap
            Bitmap output = WorkerUtils.blurBitmap(picture, applicationContext);

            // Write bitmap to a temp file
            Uri outputUri = WorkerUtils.writeBitmapToFile(applicationContext, output);

            WorkerUtils.makeStatusNotification("Output is "
                    + outputUri.toString(), applicationContext);

            // If there were no errors, return SUCCESS
            return Result.success();
        } catch (Throwable throwable) {

            // Technically WorkManager will return Result.failure()
            // but it's best to be explicit about it.
            // Thus if there were errors, we're return FAILURE
            Log.e(TAG, "Error applying blur", throwable);
            return Result.failure();
        }
    }
}

Etapa 4: acessar a WorkManager no ViewModel

Crie uma variável para uma instância de WorkManager no ViewModel e a instancie no construtor do ViewModel:

BlurViewModel.java

private WorkManager mWorkManager;

// BlurViewModel constructor
public BlurViewModel(@NonNull Application application) {
  super(application);
  mWorkManager = WorkManager.getInstance(application);

  //...rest of the constructor
}

Etapa 5: colocar uma WorkRequest na fila na WorkManager

Muito bem. Agora vamos fazer uma WorkRequest e pedir para a WorkManager executá-la. Há dois tipos de WorkRequests:

  • OneTimeWorkRequest: uma WorkRequest que será executada apenas uma vez.
  • PeriodicWorkRequest: uma WorkRequest que será repetida em um ciclo.

Só queremos que a imagem seja desfocada uma vez quando o botão Go for clicado. O método applyBlur é chamado quando o botão Go é clicado, então crie uma OneTimeWorkRequest usando BlurWorker. Em seguida, coloque sua WorkRequest. na fila usando a instância da WorkManager.

Adicione a seguinte linha de código ao método applyBlur() do BlurViewModel:

BlurViewModel.java

void applyBlur(int blurLevel) {
   mWorkManager.enqueue(OneTimeWorkRequest.from(BlurWorker.class));
}

Etapa 6: executar o código

Execute o código. Ele será compilado e você verá a notificação quando pressionar o botão Go.

7ef0320960f4d756.png

Você também pode abrir o Device File Explorer no Android Studio:

cf10a1af6e84f5ff.png

Depois, navegue até data>data>com.example.background>files>blur_filter_outputs><URI> e confirme se o peixe foi desfocado:

7f5eba3559b44cbb.png

Desfocar a imagem de teste é muito bom, mas, para que o Blur-O-Matic seja o app revolucionário de edição de imagens que está destinado a ser, você precisa permitir que os usuários desfoquem as próprias imagens.

Para fazer isso, forneceremos o URI da imagem do usuário selecionada como entrada da nossa WorkRequest.

Etapa 1: criar um objeto de entrada Data

A entrada e a saída são transmitidas por objetos Data. Objetos Data são contêineres leves para pares de chave-valor. O objetivo deles é armazenar uma quantidade pequena de dados que podem ser transmitidos de/para WorkRequests.

O URI será transmitido da imagem do usuário para um pacote. Ele será armazenado em uma variável chamada mImageUri.

Crie um método particular chamado createInputDataForUri. Esse método vai:

  1. criar um objeto Data.Builder;
  2. se mImageUri for um URI não nulo, adicioná-lo ao objeto Data usando o método putString. Esse método usa uma chave e um valor. Você pode usar a constante KEY_IMAGE_URI de string da classe Constants;
  3. chamar build() no objeto Data.Builder para criar o objeto Data e retorná-lo.

Veja abaixo o método createInputDataForUri completo:

BlurViewModel.java

/**
 * Creates the input data bundle which includes the Uri to operate on
 * @return Data which contains the Image Uri as a String
 */
private Data createInputDataForUri() {
    Data.Builder builder = new Data.Builder();
    if (mImageUri != null) {
        builder.putString(KEY_IMAGE_URI, mImageUri.toString());
    }
    return builder.build();
}

Etapa 2: transmitir o objeto Data para a WorkRequest

Mude o método applyBlur para que ele faça o seguinte:

  1. Crie um novo OneTimeWorkRequest.Builder.
  2. Chame setInputData, transmitindo o resultado de createInputDataForUri.
  3. Crie a OneTimeWorkRequest.
  4. Coloque essa solicitação na fila usando a WorkManager.

Veja abaixo o método applyBlur completo:

BlurViewModel.java

void applyBlur(int blurLevel) {
   OneTimeWorkRequest blurRequest =
                new OneTimeWorkRequest.Builder(BlurWorker.class)
                        .setInputData(createInputDataForUri())
                        .build();

   mWorkManager.enqueue(blurRequest);
}

Etapa 3: atualizar o doWork() do BlurWorker para acessar a entrada

Agora, vamos atualizar o método doWork() do BlurWorker para acessar o URI transmitido do objeto Data:

BlurWorker.java

public Result doWork() {

       Context applicationContext = getApplicationContext();

        // ADD THIS LINE
       String resourceUri = getInputData().getString(Constants.KEY_IMAGE_URI);

        //... rest of doWork()
}

Essa variável não será usada até que você conclua as próximas etapas.

Etapa 4: desfocar o URI fornecido

Com o URI, é possível desfocar a imagem que o usuário selecionou:

BlurWorker.java

public Worker.Result doWork() {
       Context applicationContext = getApplicationContext();

       String resourceUri = getInputData().getString(Constants.KEY_IMAGE_URI);

    try {

        // REPLACE THIS CODE:
        // Bitmap picture = BitmapFactory.decodeResource(
        //        applicationContext.getResources(),
        //        R.drawable.test);
        // WITH
        if (TextUtils.isEmpty(resourceUri)) {
            Log.e(TAG, "Invalid input uri");
            throw new IllegalArgumentException("Invalid input uri");
        }

        ContentResolver resolver = applicationContext.getContentResolver();
        // Create a bitmap
        Bitmap picture = BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(resourceUri)));
        //...rest of doWork

Etapa 5: URI temporário de saída

Terminamos esse worker e agora podemos retornar Result.success(). Forneceremos o OutputURI como Data de saída para facilitar o acesso a essa imagem temporária por outros workers para mais operações. Isso será útil no próximo capítulo, quando criarmos uma cadeia de workers. Para fazer isso:

  1. Crie um novo Data, assim como fez com a entrada, e armazene outputUri como uma String. Use a mesma chave (KEY_IMAGE_URI).
  2. Transmita isso para o método Result.success() do Worker.

BlurWorker.java

Essa linha precisa seguir a linha WorkerUtils.makeStatusNotification e substituir Result.success() em doWork():

Data outputData = new Data.Builder()
    .putString(KEY_IMAGE_URI, outputUri.toString())
    .build();
return Result.success(outputData);

Etapa 6: executar o app

Agora, você executará o app. Ele precisa ser compilado e ter o mesmo comportamento.

Você também pode abrir o Device File Explorer no Android Studio e navegar até data/data/com.example.background/files/blur_filter_outputs/<URI>, como fez na última etapa.

Talvez seja necessário usar a função Synchronize para ver as imagens:

7e717ffd6b3d9d52.png

Bom trabalho! Você desfocou uma imagem de entrada usando a WorkManager.

No momento, você está fazendo uma única tarefa: desfocar a imagem. Esse é um ótimo primeiro passo, mas faltam algumas funções básicas:

  • Os arquivos temporários não são limpos.
  • A imagem não é salva em um arquivo permanente.
  • A imagem sempre é desfocada no mesmo nível.

Usaremos uma cadeia de trabalho da WorkManager para adicionar essas funções.

A WorkManager permite que você crie WorkerRequests separadas que são executadas em ordem ou paralelamente. Nesta etapa, você criará uma cadeia de trabalho semelhante a esta:

54832b34e9c9884a.png

As WorkRequests são representadas como caixas.

Outra característica interessante do encadeamento é que a saída de uma WorkRequest se torna a entrada da próxima WorkRequest na cadeia. A entrada e a saída transmitidas entre cada WorkRequest são mostradas como texto azul.

Etapa 1: criar workers de limpeza e salvamento

Primeiro, defina todas as classes de Worker necessárias. Você já tem um Worker para desfocar uma imagem, mas também precisa de um que limpe arquivos temporários e um que salve a imagem permanentemente.

Crie duas novas classes no pacote worker que ampliem Worker.

A primeira será chamada de CleanupWorker e a segunda de SaveImageToFileWorker.

Etapa 2: adicionar um construtor

Adicione um construtor à classe CleanupWorker:

public class CleanupWorker extends Worker {
    public CleanupWorker(
            @NonNull Context appContext,
            @NonNull WorkerParameters workerParams) {
        super(appContext, workerParams);
    }
}

Etapa 3: substituir e implementar doWork() para CleanerWorker

O CleanupWorker não precisa receber nenhuma entrada nem transmitir nenhuma saída. Os arquivos temporários, se houver, serão sempre excluídos. Como este não é um codelab sobre manipulação de arquivos, você pode copiar o código para o CleanupWorker abaixo:

CleanupWorker.java

import android.content.Context;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.example.background.Constants;
import java.io.File;

public class CleanupWorker extends Worker {
    public CleanupWorker(
            @NonNull Context appContext,
            @NonNull WorkerParameters workerParams) {
        super(appContext, workerParams);
    }

    private static final String TAG = CleanupWorker.class.getSimpleName();

    @NonNull
    @Override
    public Result doWork() {
        Context applicationContext = getApplicationContext();

        // Makes a notification when the work starts and slows down the work so that it's easier to
        // see each WorkRequest start, even on emulated devices
        WorkerUtils.makeStatusNotification("Cleaning up old temporary files",
                applicationContext);
        WorkerUtils.sleep();

        try {
            File outputDirectory = new File(applicationContext.getFilesDir(),
                    Constants.OUTPUT_PATH);
            if (outputDirectory.exists()) {
                File[] entries = outputDirectory.listFiles();
                if (entries != null && entries.length > 0) {
                    for (File entry : entries) {
                        String name = entry.getName();
                        if (!TextUtils.isEmpty(name) && name.endsWith(".png")) {
                            boolean deleted = entry.delete();
                            Log.i(TAG, String.format("Deleted %s - %s",
                                    name, deleted));
                        }
                    }
                }
            }

            return Worker.Result.success();
        } catch (Exception exception) {
            Log.e(TAG, "Error cleaning up", exception);
            return Worker.Result.failure();
        }
    }
}

Etapa 4: substituir e implementar doWork() para SaveImageToFileWorker

O SaveImageToFileWorker receberá entrada e saída. A entrada é uma String armazenada com a chave KEY_IMAGE_URI. A saída também será uma String armazenada com a chave KEY_IMAGE_URI.

475a08a82ea675ca.png

Como este ainda não é um codelab sobre manipulação de arquivos, o código é apresentado abaixo, com dois TODOs para você preencher o código adequado para entrada e saída. Ele é muito parecido com o código que você programou na última etapa para entrada e saída, já que usa as mesmas chaves.

SaveImageToFileWorker.java

import android.content.ContentResolver;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.net.Uri;
import android.provider.MediaStore;
import android.text.TextUtils;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.work.Data;
import androidx.work.Worker;
import androidx.work.WorkerParameters;
import com.example.background.Constants;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;

public class SaveImageToFileWorker extends Worker {
    public SaveImageToFileWorker(
            @NonNull Context appContext,
            @NonNull WorkerParameters workerParams) {
        super(appContext, workerParams);
    }

    private static final String TAG = SaveImageToFileWorker.class.getSimpleName();

    private static final String TITLE = "Blurred Image";
    private static final SimpleDateFormat DATE_FORMATTER =
            new SimpleDateFormat("yyyy.MM.dd 'at' HH:mm:ss z", Locale.getDefault());

    @NonNull
    @Override
    public Result doWork() {
        Context applicationContext = getApplicationContext();

        // Makes a notification when the work starts and slows down the work so that it's easier to
        // see each WorkRequest start, even on emulated devices
        WorkerUtils.makeStatusNotification("Saving image", applicationContext);
        WorkerUtils.sleep();

        ContentResolver resolver = applicationContext.getContentResolver();
        try {
            String resourceUri = getInputData()
                    .getString(Constants.KEY_IMAGE_URI);
            Bitmap bitmap = BitmapFactory.decodeStream(
                    resolver.openInputStream(Uri.parse(resourceUri)));
            String outputUri = MediaStore.Images.Media.insertImage(
                    resolver, bitmap, TITLE, DATE_FORMATTER.format(new Date()));
            if (TextUtils.isEmpty(outputUri)) {
                Log.e(TAG, "Writing to MediaStore failed");
                return Result.failure();
            }
            Data outputData = new Data.Builder()
                    .putString(Constants.KEY_IMAGE_URI, outputUri)
                    .build();
            return Result.success(outputData);
        } catch (Exception exception) {
            Log.e(TAG, "Unable to save image to Gallery", exception);
            return Worker.Result.failure();
        }
    }
}

Etapa 5: modificar a notificação do BlurWorker

Agora que temos uma cadeia de Workers para salvar a imagem na pasta correta, podemos modificar a notificação para informar aos usuários quando o trabalho for iniciado e ficar lento para facilitar a visualização do início de cada WorkRequest, mesmo em dispositivos emulados. A versão final do BlurWorker fica assim:

BlurWorker.java

@NonNull
@Override
public Worker.Result doWork() {

    Context applicationContext = getApplicationContext();

    // Makes a notification when the work starts and slows down the work so that it's easier to
    // see each WorkRequest start, even on emulated devices
    WorkerUtils.makeStatusNotification("Blurring image", applicationContext);
    WorkerUtils.sleep();
    String resourceUri = getInputData().getString(KEY_IMAGE_URI);

    try {

        if (TextUtils.isEmpty(resourceUri)) {
            Log.e(TAG, "Invalid input uri");
            throw new IllegalArgumentException("Invalid input uri");
        }

        ContentResolver resolver = applicationContext.getContentResolver();
        // Create a bitmap
        Bitmap picture = BitmapFactory.decodeStream(
                resolver.openInputStream(Uri.parse(resourceUri)));

        // Blur the bitmap
        Bitmap output = WorkerUtils.blurBitmap(picture, applicationContext);

        // Write bitmap to a temp file
        Uri outputUri = WorkerUtils.writeBitmapToFile(applicationContext, output);

        Data outputData = new Data.Builder()
                .putString(KEY_IMAGE_URI, outputUri.toString())
                .build();

        // If there were no errors, return SUCCESS
        return Result.success(outputData);
    } catch (Throwable throwable) {

        // Technically WorkManager will return Result.failure()
        // but it's best to be explicit about it.
        // Thus if there were errors, we're return FAILURE
        Log.e(TAG, "Error applying blur", throwable);
        return Result.failure();
    }
}

Etapa 6: criar uma cadeia de WorkRequest

Você precisa modificar o método applyBlur do BlurViewModel para executar uma cadeia de WorkRequests em vez de apenas uma delas. Atualmente, o código é assim:

BlurViewModel.java

void applyBlur(int blurLevel) {
    OneTimeWorkRequest blurRequest =
            new OneTimeWorkRequest.Builder(BlurWorker.class)
                    .setInputData(createInputDataForUri())
                    .build();

    mWorkManager.enqueue(blurRequest);
}

Em vez de chamar WorkManager.enqueue(), chame WorkManager.beginWith(). Isso retorna uma WorkContinuation, que define uma cadeia de WorkRequests. Você pode adicionar itens a essa cadeia de solicitações de trabalho chamando o método then(). Por exemplo, se tiver três objetos WorkRequest (workA, workB e workC), você pode fazer o seguinte:

// Example code. Don't copy to the project
WorkContinuation continuation = mWorkManager.beginWith(workA);

continuation.then(workB) // FYI, then() returns a new WorkContinuation instance
        .then(workC)
        .enqueue(); // Enqueues the WorkContinuation which is a chain of work

Isso produziria e executaria a seguinte cadeia de WorkRequests:

2c4bf31e5f6522ad.png

Crie uma cadeia com uma WorkRequest CleanupWorker, uma BlurImage WorkRequest e uma SaveImageToFile WorkRequest em applyBlur. Transmita a entrada para a BlurImage WorkRequest.

O código resultante será o seguinte:

BlurViewModel.java

void applyBlur(int blurLevel) {

    // Add WorkRequest to Cleanup temporary images
    WorkContinuation continuation =
        mWorkManager.beginWith(OneTimeWorkRequest.from(CleanupWorker.class));

    // Add WorkRequest to blur the image
    OneTimeWorkRequest blurRequest = new OneTimeWorkRequest.Builder(BlurWorker.class)
                    .setInputData(createInputDataForUri())
                    .build();
    continuation = continuation.then(blurRequest);

    // Add WorkRequest to save the image to the filesystem
    OneTimeWorkRequest save =
        new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
            .build();
    continuation = continuation.then(save);

    // Actually start the work
    continuation.enqueue();
}

Ele será compilado e executado. A imagem que você escolheu desfocar será salva na pasta Imagens:

e2d29f34bdf01860.png

Etapa 7: repetir o BlurWorker

Está na hora de adicionar um recurso para desfocar a imagem em níveis diferentes. Pegue o parâmetro blurLevel transmitido para applyBlur e adicione essa quantidade de operações WorkRequest de desfoque à cadeia. Apenas a primeira WorkRequest precisa receber a entrada do URI.

Faça o teste e compare com o código abaixo:

BlurViewModel.java

void applyBlur(int blurLevel) {

    // Add WorkRequest to Cleanup temporary images
    WorkContinuation continuation = mWorkManager.beginWith(OneTimeWorkRequest.from(CleanupWorker.class));

    // Add WorkRequests to blur the image the number of times requested
    for (int i = 0; i < blurLevel; i++) {
        OneTimeWorkRequest.Builder blurBuilder =
                new OneTimeWorkRequest.Builder(BlurWorker.class);

        // Input the Uri if this is the first blur operation
        // After the first blur operation the input will be the output of previous
        // blur operations.
        if ( i == 0 ) {
            blurBuilder.setInputData(createInputDataForUri());
        }

        continuation = continuation.then(blurBuilder.build());
    }

    // Add WorkRequest to save the image to the filesystem
    OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
            .build();
    continuation = continuation.then(save);

    // Actually start the work
    continuation.enqueue();
}

Ótimo trabalho! Agora você pode desfocar uma imagem o quanto quiser. Quanto mistério!

fcb326118dd99959.png

Agora que você já usou as cadeias, está na hora de abordar outro recurso poderoso da WorkManager: cadeias de trabalho únicas.

Às vezes, você quer que apenas uma cadeia de trabalho seja executada por vez. Por exemplo, talvez você tenha uma cadeia de trabalho que sincroniza os dados locais com o servidor, então é recomendável deixar a primeira sincronização de dados terminar antes de iniciar uma nova. Para fazer isso, use beginUniqueWork em vez de beginWith e forneça um nome de String exclusivo. Isso nomeia toda a cadeia de solicitações de trabalho para que você possa consultá-las em conjunto.

Use beginUniqueWork para garantir que a cadeia de trabalho para desfoque do arquivo seja única. Transmita IMAGE_MANIPULATION_WORK_NAME como a chave. Você também precisa transmitir uma ExistingWorkPolicy. Suas opções são REPLACE, KEEP ou APPEND.

Você usará REPLACE porque, se o usuário decidir desfocar outra imagem antes que a atual seja concluída, precisamos interromper o processo atual e começar a desfocar a nova imagem.

O código para iniciar a continuação de trabalho único é mostrado abaixo:

BlurViewModel.java

// REPLACE THIS CODE:
// WorkContinuation continuation =
// mWorkManager.beginWith(OneTimeWorkRequest.from(CleanupWorker.class));
// WITH
WorkContinuation continuation = mWorkManager
                .beginUniqueWork(IMAGE_MANIPULATION_WORK_NAME,
                       ExistingWorkPolicy.REPLACE,
                       OneTimeWorkRequest.from(CleanupWorker.class));

O Blur-O-Matic agora desfocará apenas uma imagem por vez.

Esta seção usa muito LiveData. Por isso, você precisa conhecê-lo para entender totalmente o que está acontecendo. O LiveData é um armazenador de dados observáveis com reconhecimento de ciclo de vida.

Consulte a documentação ou o codelab Componentes compatíveis com ciclo de vida do Android se esta for a primeira vez que você trabalha com o LiveData ou os observáveis.

A próxima grande mudança que você fará é mudar o que é exibido no app durante a execução do trabalho.

Você pode ver o status de qualquer WorkRequest acessando um LiveData que tenha um objeto WorkInfo. WorkInfo é um objeto que contém detalhes sobre o estado atual de uma WorkRequest, incluindo:

A tabela a seguir mostra três maneiras diferentes de acessar objetos LiveData<WorkInfo> ou LiveData<List<WorkInfo>> e o que cada uma faz.

Tipo

Método da WorkManager

Descrição

Acessar o trabalho usando um ID

getWorkInfoByIdLiveData

Cada WorkRequest tem um ID exclusivo gerado pela WorkManager. É possível usá-lo para acessar um único LiveData
para essa WorkRequest específica.

Acessar o trabalho usando um nome da cadeia única

getWorkInfosForUniqueWorkLiveData

Como você acabou de ver, as WorkRequests podem fazer parte de uma cadeia única. Isso retorna LiveData
>
para todo o trabalho em uma cadeia única de WorkRequests.

Acessar o trabalho usando uma tag

getWorkInfosByTagLiveData

Por fim, você pode incluir uma tag em qualquer WorkRequest com uma string. Inclua a mesma tag em várias WorkRequests para associá-las. Isso retorna o LiveData
>
para qualquer tag única.

Você incluirá uma tag na WorkRequest SaveImageToFileWorker para poder acessá-la usando getWorkInfosByTagLiveData. Você usará uma tag para identificar o trabalho em vez de usar o ID da WorkManager porque, se o usuário desfocar várias imagens, todas as WorkRequests de salvamento de imagem terão a mesma tag, mas não o mesmo ID. Também é possível escolher a tag.

Você não usaria getWorkInfosForUniqueWorkLiveData, porque isso retornaria WorkInfo para todas as WorkRequests de desfoque e a WorkRequest de limpeza também. Seria necessário usar uma lógica extra para encontrar a WorkRequest de salvamento de imagem.

Etapa 1: incluir uma tag no trabalho

Em applyBlur, ao criar o SaveImageToFileWorker, inclua uma tag no trabalho usando a constante TAG_OUTPUT de String:

BlurViewModel.java

OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
        .addTag(TAG_OUTPUT) // This adds the tag
        .build();

Etapa 2: acessar o WorkInfo

Agora que você incluiu uma tag no trabalho, é possível acessar o WorkInfo:

  1. Declare uma nova variável chamada mSavedWorkInfo, que é um LiveData<List<WorkInfo>>.
  2. No construtor BlurViewModel, acesse WorkInfo usando WorkManager.getWorkInfosByTagLiveData.
  3. Adicione um getter para mSavedWorkInfo.

O código necessário é o seguinte:

BlurViewModel.java

// New instance variable for the WorkInfo class
private LiveData<List<WorkInfo>> mSavedWorkInfo;

// Placed this code in the BlurViewModel constructor
mSavedWorkInfo = mWorkManager.getWorkInfosByTagLiveData(TAG_OUTPUT);

// Add a getter method for mSavedWorkInfo
LiveData<List<WorkInfo>> getOutputWorkInfo() { return mSavedWorkInfo; }

Etapa 3: exibir o WorkInfo

Agora que você tem um LiveData para seu WorkInfo, é possível observá-lo na BlurActivity. No observador:

  1. Confira se a lista de WorkInfo não é nula e se ela tem objetos WorkInfo. Se não tiver, isso significa que o botão Go ainda não foi clicado, então retorne.
  2. Acesse o primeiro WorkInfo na lista. Haverá somente um WorkInfo marcado com TAG_OUTPUT porque tornamos a cadeia de trabalho única.
  3. Use workInfo.getState().isFinished(); para conferir se o trabalho tem um status concluído.
  4. Se não estiver concluído, chame showWorkInProgress(), que oculta e mostra as visualizações adequadas.
  5. Se estiver concluído, chame showWorkFinished(), que oculta e mostra as visualizações adequadas.

O código fica assim:

BlurActivity.java

// Show work status, added in onCreate()
mViewModel.getOutputWorkInfo().observe(this, listOfWorkInfos -> {

    // If there are no matching work info, do nothing
    if (listOfWorkInfos == null || listOfWorkInfos.isEmpty()) {
        return;
    }

    // We only care about the first output status.
    // Every continuation has only one worker tagged TAG_OUTPUT
    WorkInfo workInfo = listOfWorkInfos.get(0);

    boolean finished = workInfo.getState().isFinished();
    if (!finished) {
        showWorkInProgress();
    } else {
        showWorkFinished();
    }
});

Etapa 4: executar o app

Execute o app. Ele será compilado e executado e agora mostrará uma barra de progresso quando estiver funcionando, assim como o botão de cancelamento:

b7d8d3182f91ce23.png

Cada WorkInfo também tem um método getOutputData, que permite acessar o objeto Data de saída com a imagem salva final. Exibiremos um botão com a mensagem See File sempre que uma imagem desfocada estiver pronta para exibição.

Etapa 1: criar mOutputUri

Crie uma variável no BlurViewModel para o URI final e forneça getters e setters. Para transformar uma String em um Uri, use o método uriOrNull.

Use o código abaixo:

BlurViewModel.java

// New instance variable for the WorkInfo
private Uri mOutputUri;

// Add a getter and setter for mOutputUri
void setOutputUri(String outputImageUri) {
    mOutputUri = uriOrNull(outputImageUri);
}

Uri getOutputUri() { return mOutputUri; }

Etapa 2: criar o botão "See File"

Já existe um botão no layout activity_blur.xml que está oculto. Ele está na BlurActivity e pode ser acessado pela vinculação de visualizações como seeFileButton.

Configure o listener de clique para esse botão. Ele precisa acessar o URI e abrir uma atividade para vê-lo. Use o código abaixo:

BlurActivity.java

// Inside onCreate()

binding.seeFileButton.setOnClickListener(view -> {
    Uri currentUri = mViewModel.getOutputUri();
    if (currentUri != null) {
        Intent actionView = new Intent(Intent.ACTION_VIEW, currentUri);
        if (actionView.resolveActivity(getPackageManager()) != null) {
            startActivity(actionView);
        }
    }
});

Etapa 3: definir o URI e mostrar o botão

Há alguns ajustes finais que você precisa aplicar ao observador de WorkInfo para que isso funcione:

  1. Se o WorkInfo for concluído, acesse os dados de saída usando workInfo.getOutputData()..
  2. Em seguida, acesse o URI de saída. Lembre-se de que ele está armazenado com a chave Constants.KEY_IMAGE_URI.
  3. Se o URI não estiver vazio, ele foi salvo corretamente. Mostre o seeFileButton e chame setOutputUri no modelo de visualização com o URI.

BlurActivity.java

// Replace the observer code we added in previous steps with this one.
// Show work info, goes inside onCreate()
mViewModel.getOutputWorkInfo().observe(this, listOfWorkInfo -> {

    // If there are no matching work info, do nothing
    if (listOfWorkInfo == null || listOfWorkInfo.isEmpty()) {
        return;
    }

    // We only care about the first output status.
    // Every continuation has only one worker tagged TAG_OUTPUT
    WorkInfo workInfo = listOfWorkInfo.get(0);

    boolean finished = workInfo.getState().isFinished();
    if (!finished) {
        showWorkInProgress();
    } else {
        showWorkFinished();
        Data outputData = workInfo.getOutputData();

        String outputImageUri = outputData.getString(Constants.KEY_IMAGE_URI);

        // If there is an output file show "See File" button
        if (!TextUtils.isEmpty(outputImageUri)) {
            mViewModel.setOutputUri(outputImageUri);
            binding.seeFileButton.setVisibility(View.VISIBLE);
        }
    }
});

Etapa 4: executar o código

Execute o código. Você verá o novo botão clicável See File, que leva ao arquivo gerado:

992d0b2390600774.png

bc1dc9414fe2326e.png

Você incluiu o botão Cancel Work, então vamos adicionar o código para que ele faça algo. Com a WorkManager, é possível cancelar trabalhos usando o ID, por tag e por nome de cadeia única.

Neste caso, é recomendável cancelar o trabalho por nome de cadeia única, já que você quer cancelar todo o trabalho na cadeia, não apenas uma etapa específica.

Etapa 1: cancelar o trabalho por nome

No modelo de visualização, programe o método para cancelar o trabalho:

BlurViewModel.java

/**
 * Cancel work using the work's unique name
 */
void cancelWork() {
    mWorkManager.cancelUniqueWork(IMAGE_MANIPULATION_WORK_NAME);
}

Etapa 2: chamar o método de cancelamento

Em seguida, vincule o botão cancelButton para chamar cancelWork:

BlurActivity.java

// In onCreate()

// Hookup the Cancel button
binding.cancelButton.setOnClickListener(view -> mViewModel.cancelWork());

Etapa 3: executar e cancelar o trabalho

Execute o app. Ele deve ser compilado sem problemas. Comece a desfocar uma imagem e, em seguida, clique no botão de cancelamento. A cadeia inteira será cancelada.

bdaadc9bb25472cb.png

Por último, mas não menos importante, a WorkManager é compatível com Constraints. Para o Blur-O-Matic, você usará a restrição de que o dispositivo precisa estar sendo carregado durante o salvamento.

Etapa 1: criar e adicionar uma restrição de carregamento

Para criar um objeto Constraints, use um Constraints.Builder. Em seguida, defina as restrições que você quer e as adicione à WorkRequest, conforme mostrado abaixo:

BlurViewModel.java

// In the applyBlur method

// Create charging constraint
Constraints constraints = new Constraints.Builder()
        .setRequiresCharging(true)
        .build();

// Add WorkRequest to save the image to the filesystem
OneTimeWorkRequest save = new OneTimeWorkRequest.Builder(SaveImageToFileWorker.class)
        .setConstraints(constraints) // This adds the Constraints
        .addTag(TAG_OUTPUT)
        .build();

continuation = continuation.then(save);

Etapa 2: testar com o emulador ou o dispositivo

Agora você pode executar o Blur-O-Matic. Se você está usando um dispositivo, pode removê-lo ou conectá-lo à fonte de energia. Em um emulador, você pode mudar o status de carregamento na janela Extended controls:

c2e56295cbe73f8.png

Quando o dispositivo não estiver carregando, ele precisará ficar no estado de carregamento até que você o conecte a uma fonte de energia.

b7d8d3182f91ce23.png

Parabéns! Você concluiu o app Blu-O-Matic e, no processo, aprendeu a:

  • adicionar a WorkManager ao projeto;
  • programar uma OneOffWorkRequest;
  • usar parâmetros de entrada e saída;
  • encadear trabalhos com WorkRequests;
  • nomear cadeias WorkRequest únicas;
  • incluir tags em WorkRequests;
  • exibir WorkInfo na IU;
  • cancelar WorkRequests;
  • adicionar restrições a uma WorkRequest.

Excelente trabalho! Para ver o estado final do código e todas as modificações, confira:

Download do código final

Ou, se preferir, você pode clonar o codelab da WorkManager no GitHub:

$ git clone -b java https://github.com/googlecodelabs/android-workmanager

A WorkManager envolve muito mais do que o conteúdo abordado neste codelab, incluindo trabalho repetitivo, uma biblioteca de suporte para testes, solicitações de trabalho paralelas e mesclagem de entradas. Para saber mais, consulte a documentação da WorkManager.