Trabalho assíncrono com linhas de execução Java

Todos os apps para Android usam uma linha de execução principal para lidar com operações de IU. Chamar operações de longa duração nesta linha de execução principal pode levar a travamentos e falta de resposta. Por exemplo, se o app fizer uma solicitação de rede na linha de execução principal, a interface vai ser congelada até receber a resposta da rede. Se você usa Java, pode criar outras linhas de execução em segundo plano para processar operações de longa duração enquanto a linha de execução principal continua processando as atualizações da IU.

Este guia mostra como os desenvolvedores que usam a linguagem de programação Java podem usar um pool de linhas de execução para configurar e usar várias linhas em um app Android. Ele também mostra como definir o código a ser executado em uma linha de execução e como se comunicar entre uma dessas linhas de execução e a principal.

Bibliotecas de simultaneidade

É importante entender os princípios básicos da linha de execução e dos mecanismos subjacentes. No entanto, há muitas bibliotecas conhecidas que oferecem abstrações de nível mais alto sobre esses conceitos e utilitários prontos para uso para transmitir dados entre linhas de execução. Essas bibliotecas incluem Guava e RxJava para usuários da linguagem de programação Java e corrotinas, que recomendamos para usuários do Kotlin.

Na prática, escolha o que funciona melhor para seu app e sua equipe de desenvolvimento, embora as regras da linha de execução permaneçam as mesmas.

Visão geral dos exemplos

Com base no Guia para a arquitetura do app, os exemplos neste tópico fazem uma solicitação de rede e retornam o resultado para a linha de execução principal, onde o app poderá exibir esse resultado na tela.

Especificamente, o ViewModel chama a camada de dados na linha de execução principal para acionar a solicitação de rede. A camada de dados é responsável por mover a execução da solicitação de rede para fora da linha de execução principal e postar o resultado de volta na linha principal usando um callback.

Para mover a execução da solicitação de rede para fora da linha de execução principal, precisamos criar outras linhas no nosso app.

Criar várias linhas de execução

Um pool de linhas de execução é uma coleção gerenciada de linhas de execução que executa tarefas em paralelo a partir de uma fila. Novas tarefas são executadas em linhas de execução existentes à medida que elas ficam inativas. Para enviar uma tarefa a um pool de linhas de execução, use a interface ExecutorService. Observe que ExecutorService não tem nada a ver com Services, o componente do aplicativo Android.

Criar linhas de execução é caro. Por isso, crie um pool apenas uma vez à medida que o app for inicializado. Salve a instância de ExecutorService na classe Application ou em um contêiner de injeção de dependência. O exemplo a seguir cria um pool de quatro linhas de execução que podemos usar para executar tarefas em segundo plano.

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
}

Há outras maneiras de configurar um pool de linhas de execução, dependendo da carga de trabalho esperada. Consulte Como configurar um pool de linhas de execução para saber mais.

Executar em uma linha de execução em segundo plano

Fazer uma solicitação de rede na linha de execução principal faz com que ela aguarde ou seja bloqueada até receber uma resposta. Como a linha de execução está bloqueada, o SO não pode chamar onDraw(), e o app trava, possivelmente levando a uma caixa de diálogo "O app não está respondendo" (ANR). Em vez disso, vamos executar essa operação em uma linha de execução em segundo plano.

Fazer a solicitação

Primeiro, confira nossa classe LoginRepository e veja como ela está fazendo a solicitação de rede:

// Result.java
public abstract class Result<T> {
    private Result() {}

    public static final class Success<T> extends Result<T> {
        public T data;

        public Success(T data) {
            this.data = data;
        }
    }

    public static final class Error<T> extends Result<T> {
        public Exception exception;

        public Error(Exception exception) {
            this.exception = exception;
        }
    }
}

// LoginRepository.java
public class LoginRepository {

    private final String loginUrl = "https://example.com/login";
    private final LoginResponseParser responseParser;

    public LoginRepository(LoginResponseParser responseParser) {
        this.responseParser = responseParser;
    }

    public Result<LoginResponse> makeLoginRequest(String jsonBody) {
        try {
            URL url = new URL(loginUrl);
            HttpURLConnection httpConnection = (HttpURLConnection) url.openConnection();
            httpConnection.setRequestMethod("POST");
            httpConnection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
            httpConnection.setRequestProperty("Accept", "application/json");
            httpConnection.setDoOutput(true);
            httpConnection.getOutputStream().write(jsonBody.getBytes("utf-8"));

            LoginResponse loginResponse = responseParser.parse(httpConnection.getInputStream());
            return new Result.Success<LoginResponse>(loginResponse);
        } catch (Exception e) {
            return new Result.Error<LoginResponse>(e);
        }
    }
}

makeLoginRequest() é síncrono e bloqueia a linha de execução de chamada. Para modelar a resposta da solicitação de rede, temos nossa própria classe Result.

Acionar a solicitação

O ViewModel aciona a solicitação de rede quando o usuário toca, por exemplo, em um botão:

public class LoginViewModel {

    private final LoginRepository loginRepository;

    public LoginViewModel(LoginRepository loginRepository) {
        this.loginRepository = loginRepository;
    }

    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody);
    }
}

Com o código anterior, LoginViewModel está bloqueando a linha de execução principal ao fazer a solicitação de rede. Podemos usar o pool de linhas de execução que instanciamos para mover a execução para uma linha de execução em segundo plano.

Processar a injeção de dependência

Primeiro, seguindo os princípios de injeção de dependência, o LoginRepository usa uma instância de Executor em vez de ExecutorService, porque está executando o código e não gerenciando linhas de execução:

public class LoginRepository {
    ...
    private final Executor executor;

    public LoginRepository(LoginResponseParser responseParser, Executor executor) {
        this.responseParser = responseParser;
        this.executor = executor;
    }
    ...
}

O método execute() do Executor usa um Runnable. Um Runnable é uma interface de método abstrato único (SAM, na sigla em inglês) com um método run() que é executado em uma linha de execução quando invocado.

Executar em segundo plano

Vamos criar outra função chamada makeLoginRequest(), que move a execução para a linha de execução em segundo plano e ignora a resposta por enquanto:

public class LoginRepository {
    ...
    public void makeLoginRequest(final String jsonBody) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                Result<LoginResponse> ignoredResponse = makeSynchronousLoginRequest(jsonBody);
            }
        });
    }

    public Result<LoginResponse> makeSynchronousLoginRequest(String jsonBody) {
        ... // HttpURLConnection logic
    }
    ...
}

Dentro do método execute(), criamos um novo Runnable com o bloco de código que queremos executar na linha de execução em segundo plano. No nosso caso, é o método síncrono de solicitação de rede. Internamente, o ExecutorService gerencia a Runnable e a executa em uma linha de execução disponível.

considerações

Qualquer linha de execução no seu app pode ser executada em paralelo com outras, incluindo a principal. Portanto, verifique se o código é seguro para linhas de execução. No nosso exemplo, evitamos gravar em variáveis compartilhadas entre linhas de execução, transmitindo dados imutáveis. Essa é uma boa prática, porque cada linha de execução funciona com a própria instância de dados, e nós evitamos a complexidade da sincronização.

Se você precisar compartilhar o estado entre linhas de execução, tenha cuidado para gerenciar o acesso das linhas de execução usando mecanismos de sincronização, como bloqueios. Isso está fora do escopo deste guia. Em geral, evite compartilhar o estado mutável entre as linhas de execução sempre que possível.

Comunicar-se com a linha de execução principal

Na etapa anterior, ignoramos a resposta da solicitação de rede. Para mostrar o resultado na tela, LoginViewModel precisa saber sobre ele. Podemos fazer isso usando callbacks.

A função makeLoginRequest() precisa receber um callback como parâmetro para que possa retornar um valor de forma assíncrona. O callback com o resultado é chamado sempre que a solicitação de rede é concluída ou ocorre uma falha. Em Kotlin, podemos usar uma função de ordem superior. No entanto, em Java, precisamos criar uma nova interface de callback para ter a mesma funcionalidade:

interface RepositoryCallback<T> {
    void onComplete(Result<T> result);
}

public class LoginRepository {
    ...
    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    callback.onComplete(result);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    callback.onComplete(errorResult);
                }
            }
        });
    }
  ...
}

O ViewModel precisa implementar o callback agora. Ele pode executar uma lógica diferente dependendo do resultado:

public class LoginViewModel {
    ...
    public void makeLoginRequest(String username, String token) {
        String jsonBody = "{ username: \"" + username + "\", token: \"" + token + "\" }";
        loginRepository.makeLoginRequest(jsonBody, new RepositoryCallback<LoginResponse>() {
            @Override
            public void onComplete(Result<LoginResponse> result) {
                if (result instanceof Result.Success) {
                    // Happy path
                } else {
                    // Show error in UI
                }
            }
        });
    }
}

Neste exemplo, o callback é executado na linha de execução de chamada, que é uma linha de execução em segundo plano. Isso significa que não é possível modificar nem se comunicar diretamente com a camada de interface até que você volte para a linha de execução principal.

Usar gerenciadores

Você pode usar um Handler para enfileirar uma ação a ser realizada em uma linha de execução diferente. Para especificar a linha de execução em que a ação será executada, construa o Handler usando um Looper para a linha de execução. Um Looper é um objeto que executa o loop de mensagens para uma linha de execução associada. Depois de criar um Handler, você pode usar o método post(Runnable) para executar um bloco de código na linha de execução correspondente.

Looper inclui uma função auxiliar, getMainLooper(), que recupera o Looper da linha de execução principal. Você pode executar o código na linha de execução principal usando esse Looper para criar uma Handler. Como isso é algo que você pode fazer com frequência, também é possível salvar uma instância de Handler no mesmo lugar em que salvou ExecutorService:

public class MyApplication extends Application {
    ExecutorService executorService = Executors.newFixedThreadPool(4);
    Handler mainThreadHandler = HandlerCompat.createAsync(Looper.getMainLooper());
}

É uma boa prática injetar o gerenciador no repositório, porque ele oferece mais flexibilidade. Por exemplo, no futuro, convém transmitir um Handler diferente para programar tarefas em uma linha de execução separada. Se você estiver sempre se comunicando de volta para a mesma linha de execução, transmita o Handler para o construtor de repositório, conforme mostrado no exemplo abaixo.

public class LoginRepository {
    ...
    private final Handler resultHandler;

    public LoginRepository(LoginResponseParser responseParser, Executor executor,
            Handler resultHandler) {
        this.responseParser = responseParser;
        this.executor = executor;
        this.resultHandler = resultHandler;
    }

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
    ...
}

Como alternativa, se você quiser mais flexibilidade, transmita um Handler para cada função:

public class LoginRepository {
    ...

    public void makeLoginRequest(
        final String jsonBody,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler,
    ) {
        executor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Result<LoginResponse> result = makeSynchronousLoginRequest(jsonBody);
                    notifyResult(result, callback, resultHandler);
                } catch (Exception e) {
                    Result<LoginResponse> errorResult = new Result.Error<>(e);
                    notifyResult(errorResult, callback, resultHandler);
                }
            }
        });
    }

    private void notifyResult(
        final Result<LoginResponse> result,
        final RepositoryCallback<LoginResponse> callback,
        final Handler resultHandler
    ) {
        resultHandler.post(new Runnable() {
            @Override
            public void run() {
                callback.onComplete(result);
            }
        });
    }
}

Neste exemplo, o callback transmitido para a chamada makeLoginRequest do repositório é executado na linha de execução principal. Isso significa que é possível modificar diretamente a interface do callback ou usar LiveData.setValue() para se comunicar com a interface.

Configurar um pool de linhas de execução

Você pode criar um pool de linhas de execução usando uma das funções auxiliares Executor com configurações predefinidas, conforme mostrado no código de exemplo anterior. Como alternativa, se você quiser personalizar os detalhes do pool de linhas de execução, crie uma instância usando diretamente ThreadPoolExecutor. É possível configurar os seguintes detalhes:

  • Tamanho inicial e máximo do pool.
  • Tempo de sinal de atividade e unidade de tempo. O tempo de atividade é o tempo máximo que uma linha de execução pode permanecer inativa antes de ser encerrada.
  • Uma fila de entrada que contém tarefas Runnable. Essa fila precisa implementar a interface BlockingQueue. Para atender aos requisitos do seu aplicativo, você pode escolher entre as implementações de fila disponíveis. Para saber mais, consulte a visão geral da classe de ThreadPoolExecutor.

Confira um exemplo que especifica o tamanho do pool de linhas de execução com base no número total de núcleos de processador, no tempo de sinal de atividade de um segundo e em uma fila de entrada.

public class MyApplication extends Application {
    /*
     * Gets the number of available cores
     * (not always the same as the maximum number of cores)
     */
    private static int NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors();

    // Instantiates the queue of Runnables as a LinkedBlockingQueue
    private final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>();

    // Sets the amount of time an idle thread waits before terminating
    private static final int KEEP_ALIVE_TIME = 1;
    // Sets the Time Unit to seconds
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;

    // Creates a thread pool manager
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            NUMBER_OF_CORES,       // Initial pool size
            NUMBER_OF_CORES,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            workQueue
    );
    ...
}