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 interfaceBlockingQueue
. 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 deThreadPoolExecutor
.
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.