Como executar tarefas do Android em linhas de execução em segundo plano

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 dessa linha de execução principal pode levar a travamentos e falhas de resposta. Por exemplo, se o app fizer uma solicitação de rede pela linha de execução principal, a IU dele será congelada até receber a resposta da rede. É possível criar outras linhas de execução em segundo plano para lidar com operações de longa duração enquanto a linha de execução principal continua processando as atualizações da IU.

Este guia mostra aos desenvolvedores das linguagens de programação Kotlin e Java como usar um pool de linhas de execução para configurar e usar várias linhas em um app para Android. Este guia 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 e a principal.

Visão geral dos exemplos

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

Especificamente, o ViewModel chama a camada de repositório na linha de execução principal para acionar a solicitação de rede. A camada do repositório é responsável por mover a execução da solicitação de rede para fora da linha de execução principal e publicar o resultado nela usando um callback.

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

Como criar várias linhas de execução

Um pool de linhas de execução é uma coleção gerenciada 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 o ExecutorService não tem nada a ver com Services, o componente de aplicativo do Android.

A criação de linhas de execução é cara. Portanto, crie um pool apenas uma vez à medida que o aplicativo for inicializado. Salve a instância do ExecutorService na classe Application ou em um contêiner de injeção de dependências. O exemplo a seguir cria um pool de quatro linhas de execução que podem ser usadas para executar tarefas em segundo plano.

Kotlin

class MyApplication : Application() {
    val executorService: ExecutorService = Executors.newFixedThreadPool(4)
}

Java

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 mais informações.

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 ela está bloqueada, o SO não pode chamar onDraw(). Seu app trava, possivelmente levando a uma caixa de diálogo "O app não está respondendo" (ANR, na sigla em inglês). Em vez disso, vamos executar essa operação em uma linha de execução em segundo plano.

Primeiro, vejamos nossa classe Repository e como ela está fazendo a solicitação de rede:

Kotlin

sealed class Result<out R> {
    data class Success<out T>(val data: T) : Result<T>()
    data class Error(val exception: Exception) : Result<Nothing>()
}

class LoginRepository(private val responseParser: LoginResponseParser) {
    private const val loginUrl = "https://example.com/login"

    // Function that makes the network request, blocking the current thread
    fun makeLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        val url = URL(loginUrl)
        (url.openConnection() as? HttpURLConnection)?.run {
            requestMethod = "POST"
            setRequestProperty("Content-Type", "application/json; charset=utf-8")
            setRequestProperty("Accept", "application/json")
            doOutput = true
            outputStream.write(jsonBody.toByteArray())

            return Result.Success(responseParser.parse(inputStream))
        }
        return Result.Error(Exception("Cannot open HttpURLConnection"))
    }
}

Java

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

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

Kotlin

class LoginViewModel(
    private val loginRepository: LoginRepository
) {
    fun makeLoginRequest(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody)
    }
}

Java

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, o 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 em segundo plano. Primeiro, seguindo os princípios da injeção de dependências, LoginRepository usa uma instância de Executor em vez de ExecutorService porque está executando código e não gerenciando linhas de execução:

Kotlin

class LoginRepository(
    private val responseParser: LoginResponseParser
    private val executor: Executor
) { ... }

Java

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 Simples (SAM, na sigla em inglês) com um método run() executado em uma linha de execução quando invocado.

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:

Kotlin

class LoginRepository(
    private val responseParser: LoginResponseParser
    private val executor: Executor
) {

    fun makeLoginRequest(jsonBody: String) {
        executor.execute {
            val ignoredResponse = makeSynchronousLoginRequest(url, jsonBody)
        }
    }

    private fun makeSynchronousLoginRequest(
        jsonBody: String
    ): Result<LoginResponse> {
        ... // HttpURLConnection logic
    }
}

Java

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 de solicitação de rede síncrona. Internamente, o ExecutorService gerencia o Runnable e o executa em uma linha de execução disponível.

Considerações

Qualquer linha de execução do 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 escrever em variáveis compartilhadas entre linhas de execução, passando dados imutáveis. Essa é uma prática recomendada, porque cada linha de execução funciona com a própria instância de dados. Assim, evitamos a complexidade da sincronização.

Se você precisar compartilhar o estado entre linhas de execução, tenha cuidado para gerenciar o acesso de linhas 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 sempre que possível.

Como se comunicar com a linha de execução principal

Na etapa anterior, ignoramos a resposta da solicitação de rede. Para exibir 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. No 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:

Kotlin

class LoginRepository(
    private val responseParser: LoginResponseParser
    private val executor: Executor
) {

    fun makeLoginRequest(
        jsonBody: String,
        callback: (Result<LoginResponse>) -> Unit
    ) {
        executor.execute {
            try {
                val response = makeSynchronousLoginRequest(jsonBody)
                callback(response)
            } catch (e: Exception) {
                val errorResult = Result.Error(e)
                callback(errorResult)
            }
        }
    }
    ...
}

Java

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:

Kotlin

class LoginViewModel(
    private val loginRepository: LoginRepository
) {
    fun makeLoginRequest(username: String, token: String) {
        val jsonBody = "{ username: \"$username\", token: \"$token\"}"
        loginRepository.makeLoginRequest(jsonBody) { result ->
            when(result) {
                is Result.Success<LoginResponse> -> // Happy path
                else -> // Show error in UI
            }
        }
    }
}

Java

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

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

Como usar gerenciadores

Você pode usar um Handler para enfileirar uma ação a ser executada em uma linha de execução diferente. Para especificar a linha 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, use o método post(Runnable) para executar um bloco de código na linha de execução correspondente.

O Looper inclui uma função auxiliar, getMainLooper(), que recupera o Looper da linha de execução principal. Execute o código na linha de execução principal usando este Looper para criar um Handler. Como isso pode ser feito com frequência, também é possível salvar uma instância do Handler no mesmo lugar em que salvou o ExecutorService:

Kotlin

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

Java

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

É uma prática recomendada injetar o gerenciador no Repository, já que ele oferece mais flexibilidade. Por exemplo, no futuro, convém passar um Handler diferente para programar tarefas em uma linha de execução diferente. Se você estiver sempre se comunicando com a mesma linha de execução, passe o Handler para o construtor Repository, conforme mostrado no exemplo a seguir.

Kotlin

class LoginRepository(
    ...
    private val resultHandler: Handler
) {

    fun makeLoginRequest(
        jsonBody: String,
        callback: (Result<LoginResponse>) -> Unit
    ) {
          executor.execute {
              try {
                  val response = makeSynchronousLoginRequest(jsonBody)
                  resultHandler.post { callback(response) }
              } catch (e: Exception) {
                  val errorResult = Result.Error(e)
                  resultHandler.post { callback(errorResult) }
              }
          }
    }
    ...
}

Java

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, poderá passar um gerenciador para cada função:

Kotlin

class LoginRepository(...) {
    ...
    fun makeLoginRequest(
        jsonBody: String,
        resultHandler: Handler,
        callback: (Result<LoginResponse>) -> Unit
    ) {
        executor.execute {
            try {
                val response = makeSynchronousLoginRequest(jsonBody)
                resultHandler.post { callback(response) }
            } catch (e: Exception) {
                val errorResult = Result.Error(e)
                resultHandler.post { callback(errorResult) }
            }
        }
    }
}

Java

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 você pode modificar a IU diretamente pelo callback ou usar LiveData.setValue() para se comunicar com a IU.

Como 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, poderá criar uma instância usando ThreadPoolExecutor diretamente. Você pode configurar os seguintes detalhes:

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

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

Kotlin

class MyApplication : Application() {
    /*
     * Gets the number of available cores
     * (not always the same as the maximum number of cores)
     */
    private val NUMBER_OF_CORES = Runtime.getRuntime().availableProcessors()

    // Instantiates the queue of Runnables as a LinkedBlockingQueue
    private val workQueue: BlockingQueue<Runnable> =
            LinkedBlockingQueue<Runnable>()

    // Sets the amount of time an idle thread waits before terminating
    private const val KEEP_ALIVE_TIME = 1L
    // Sets the Time Unit to seconds
    private val KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS
    // Creates a thread pool manager
    private val threadPoolExecutor: ThreadPoolExecutor = ThreadPoolExecutor(
            NUMBER_OF_CORES,       // Initial pool size
            NUMBER_OF_CORES,       // Max pool size
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            workQueue
    )
}

Java

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

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 superior sobre esses conceitos e utilitários prontos para uso para transmitir dados entre linhas de execução. Essas bibliotecas incluem a Guava e a RxJava para os usuários da linguagem de programação Java e corrotinas, recomendadas para usuários do Kotlin.

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

Veja também

Para mais informações sobre processos e linhas de execução no Android, consulte Visão geral dos processos e das linhas de execução.