Corrotinas do Kotlin permitem escrever um código assíncrono limpo e simplificado que mantém o app responsivo e ao gerenciar tarefas de longa duração, como chamadas de rede ou operações de disco.
Este tópico apresenta uma visão detalhada das corrotinas no Android. Se não conhecê-las, leia sobre corrotinas de Kotlin no Android antes deste tópico.
Gerenciar tarefas de longa duração
As corrotinas são criadas com base em funções regulares, adicionando duas operações para lidar
com tarefas de longa duração. Além de invoke
(ou call
) e return
,
as corrotinas adicionam suspend
e resume
:
suspend
pausa a execução da corrotina atual, salvando todas as variáveis locais.resume
continua a execução de uma corrotina suspensa do local onde ela foi suspensa.
É possível chamar funções suspend
somente por outras funções suspend
ou
usando um builder de corrotinas, por exemplo, launch
, para iniciar uma nova corrotina.
O exemplo a seguir mostra uma implementação simples de corrotina para uma tarefa hipotética de longa duração:
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("https://developer.android.com") // Dispatchers.IO for `get`
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = withContext(Dispatchers.IO) { /* ... */ }
Nesse exemplo, get()
ainda é executado na linha de execução principal, mas suspende a
corrotina antes de iniciar a solicitação de rede. Quando a solicitação de rede
é concluída, get
retoma a corrotina suspensa em vez de usar um callback
para notificar a linha de execução principal.
O Kotlin usa um frame de pilha para gerenciar qual função está sendo executada com qualquer variável local. Ao suspender uma corrotina, o frame de pilha atual é copiado e salvo para mais tarde. Ao retomar, o frame de pilha é copiado de onde ele foi salvo, e a função começa a ser executada novamente. Mesmo que o código possa parecer uma solicitação de bloqueio sequencial comum, a corrotina garante que a solicitação de rede evite o bloqueio da linha de execução principal.
Usar corrotinas para a segurança principal
As corrotinas Kotlin usam agentes para determinar quais linhas de execução são usadas para a execução da corrotina. Para executar o código fora da linha de execução principal, você pode pedir para as corrotinas Kotlin realizarem o trabalho no agente Padrão ou no de E/S. No Kotlin, todas as corrotinas precisam ser executadas em um agente, mesmo quando estiverem em execução na linha de execução principal. As corrotinas podem suspender a si mesmas, e o agente é responsável por reiniciá-las.
Para especificar onde as corrotinas precisam ser executadas, o Kotlin fornece três agentes para uso:
- Dispatchers.Main: use este agente para executar uma corrotina na linha de execução
principal do Android. Ele só deve ser usado para interagir com a IU e
realizar um trabalho rápido. Exemplos incluem chamar funções
suspend
, executar operações de framework de IU do Android e atualizar objetosLiveData
. - Dispatchers.IO: este agente é otimizado para executar E/S de disco ou rede fora da linha de execução principal. Exemplos incluem uso do componente Room, leitura ou gravação de arquivos e execução de qualquer operação de rede.
- Dispatchers.Default: este agente é otimizado para realizar trabalho intensivo da CPU fora da linha de execução principal. Exemplos de casos de uso incluem classificação de uma lista e análise de JSON.
Continuando com o exemplo anterior, você pode usar os agentes para redefinir a
função get
. Dentro do corpo de get
, chame withContext(Dispatchers.IO)
para
criar um bloco que é executado no pool de linhas de execução de E/S. Qualquer código que você inserir nesse
bloco sempre será executado por meio do agente IO
. Como withContext
é em si uma
função de suspensão, a função get
também é.
suspend fun fetchDocs() { // Dispatchers.Main
val result = get("developer.android.com") // Dispatchers.Main
show(result) // Dispatchers.Main
}
suspend fun get(url: String) = // Dispatchers.Main
withContext(Dispatchers.IO) { // Dispatchers.IO (main-safety block)
/* perform network IO here */ // Dispatchers.IO (main-safety block)
} // Dispatchers.Main
}
Com corrotinas, você pode enviar linhas de execução com controle refinado. Como
withContext()
permite controlar o pool de linhas de execução de qualquer linha de código sem introduzir callbacks, é possível aplicá-lo a funções muito pequenas, como ler
um banco de dados ou executar uma solicitação de rede. Uma boa prática é usar
withContext()
para garantir que todas as funções sejam protegidas, o que significa que você
pode chamar a função a partir da linha de execução principal. Dessa forma, o autor da chamada nunca precisa
pensar sobre qual linha de execução tem que ser usada para executar a função.
No exemplo anterior, fetchDocs()
é executado na linha de execução principal. No entanto,
ele pode chamar com segurança get
, que executa uma solicitação de rede em segundo plano.
Como as corrotinas são compatíveis com suspend
e resume
, a corrotina na
linha de execução principal é retomada com o resultado get
assim que o bloco withContext
é concluído.
Desempenho de withContext()
withContext()
não aumenta a sobrecarga em comparação com um modelo equivalente baseado em callback
implementação. Além disso, é possível otimizar chamadas withContext()
além de uma implementação equivalente baseada em callback em algumas situações. Por
exemplo, se uma função faz dez chamadas para uma rede, você pode dizer ao Kotlin para
alternar as linhas de execução apenas uma vez usando um withContext()
externo. Em seguida, mesmo que
a biblioteca de rede use withContext()
várias vezes, ela permanece no mesmo
agente e evita alternar linhas de execução. Além disso, o Kotlin otimiza a alternância
entre Dispatchers.Default
e Dispatchers.IO
para evitar alternância de linhas de execução
sempre que possível.
Iniciar uma corrotina
Você pode iniciar corrotinas de duas maneiras:
launch
inicia uma nova corrotina e não retorna o resultado para o autor da chamada. Qualquer trabalho que seja considerado "disparar e esquecer" pode ser iniciado usandolaunch
.async
inicia uma nova corrotina e permite retornar um resultado com uma suspensão chamadaawait
.
Normalmente, é necessário launch
uma nova corrotina a partir de uma função regular,
já que uma função regular não pode chamar await
. Use async
somente dentro
de outra corrotina ou quando estiver em uma função suspensa e realizando
a decomposição paralela.
Decomposição paralela
Como todas as corrotinas iniciadas em uma função suspend
precisam ser interrompidas quando
a função é retornada, você provavelmente precisará garantir que essas corrotinas
terminem antes do retorno. Com simultaneidade estruturada em Kotlin, você pode definir
um coroutineScope
que inicie uma ou mais corrotinas. Em seguida, usando await()
(para uma corrotina única) ou awaitAll()
(para diversas corrotinas), você pode
garantir que essas corrotinas terminem antes de retornar da função.
Como exemplo, vamos definir um coroutineScope
que busque dois documentos
de forma assíncrona. Chamando await()
em cada referência adiada, garantimos
que as duas operações async
terminem antes de retornar um valor:
suspend fun fetchTwoDocs() =
coroutineScope {
val deferredOne = async { fetchDoc(1) }
val deferredTwo = async { fetchDoc(2) }
deferredOne.await()
deferredTwo.await()
}
Você também pode usar awaitAll()
nas coleções, conforme mostrado neste exemplo:
suspend fun fetchTwoDocs() = // called on any Dispatcher (any thread, possibly Main)
coroutineScope {
val deferreds = listOf( // fetch two docs at the same time
async { fetchDoc(1) }, // async returns a result for the first doc
async { fetchDoc(2) } // async returns a result for the second doc
)
deferreds.awaitAll() // use awaitAll to wait for both network requests
}
Mesmo que fetchTwoDocs()
inicie novas corrotinas com async
, a função
usa awaitAll()
para aguardar até que as corrotinas sejam concluídas antes de
retornar. Observe, no entanto, que mesmo se não tivéssemos chamado awaitAll()
, o
builder coroutineScope
não retomaria a corrotina que chamou
fetchTwoDocs
até que todas as novas corrotinas fossem concluídas.
Além disso, coroutineScope
detecta exceções que as corrotinas lançam
e as encaminha de volta para o autor da chamada.
Para saber mais sobre decomposição paralela, consulte Como escrever funções de suspensão (link em inglês).
Conceitos de corrotinas
CoroutineScope
Uma CoroutineScope
monitora todas as corrotinas que cria usando launch
ou async
. O
trabalho em andamento (ou seja, as corrotinas em execução) pode ser cancelado chamando
scope.cancel()
a qualquer momento. No Android, algumas bibliotecas KTX fornecem
o próprio CoroutineScope
para determinadas classes de ciclo de vida. Por exemplo,
ViewModel
tem um
viewModelScope
,
e Lifecycle
tem lifecycleScope
.
Diferentemente de um agente, no entanto, um CoroutineScope
não executa as corrotinas.
viewModelScope
também é usado nos exemplos encontrados em
Linhas de execução em segundo plano no Android com corrotinas.
No entanto, se você precisar criar o próprio CoroutineScope
para controlar o
ciclo de vida de corrotinas em uma camada específica do app, faça isso
da seguinte maneira:
class ExampleClass {
// Job and Dispatcher are combined into a CoroutineContext which
// will be discussed shortly
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine within the scope
scope.launch {
// New coroutine that can call suspend functions
fetchDocs()
}
}
fun cleanUp() {
// Cancel the scope to cancel ongoing coroutines work
scope.cancel()
}
}
Um escopo cancelado não pode criar mais corrotinas. Portanto,
chame scope.cancel()
somente quando a classe que controla o ciclo de vida
estiver sendo destruída. Ao usar viewModelScope
, a classe
ViewModel
cancela o
escopo automaticamente no método onCleared()
do ViewModel.
Job
Uma Job
é um handle de uma corrotina. Cada corrotina criada com launch
ou async
retorna uma instância Job
que identifica exclusivamente a
corrotina e gerencia o ciclo de vida dela. Você também pode transmitir um Job
para um
CoroutineScope
e gerenciar ainda mais o ciclo de vida, conforme mostrado no exemplo
a seguir:
class ExampleClass {
...
fun exampleMethod() {
// Handle to the coroutine, you can control its lifecycle
val job = scope.launch {
// New coroutine
}
if (...) {
// Cancel the coroutine started above, this doesn't affect the scope
// this coroutine was launched in
job.cancel()
}
}
}
CoroutineContext
Um CoroutineContext
(link em inglês) define o comportamento de uma corrotina usando o seguinte conjunto de elementos:
Job
(link em inglês): controla o ciclo de vida da corrotina.CoroutineDispatcher
(link em inglês): envia o trabalho para a linha de execução adequada.CoroutineName
(link em inglês): o nome da corrotina, útil para depuração.CoroutineExceptionHandler
(link em inglês): processa exceções não capturadas.
Para novas corrotinas criadas em um escopo, uma nova instância Job
é
atribuída à nova corrotina, e os outros elementos CoroutineContext
são herdados do escopo que a contém. Você pode modificar os elementos herdados
transmitindo um novo CoroutineContext
para a função launch
ou async
. Observe que transmitir um Job
para launch
ou async
não tem efeito,
já que uma nova instância de Job
é sempre atribuída a uma nova corrotina.
class ExampleClass {
val scope = CoroutineScope(Job() + Dispatchers.Main)
fun exampleMethod() {
// Starts a new coroutine on Dispatchers.Main as it's the scope's default
val job1 = scope.launch {
// New coroutine with CoroutineName = "coroutine" (default)
}
// Starts a new coroutine on Dispatchers.Default
val job2 = scope.launch(Dispatchers.Default + CoroutineName("BackgroundCoroutine")) {
// New coroutine with CoroutineName = "BackgroundCoroutine" (overridden)
}
}
}
Outros recursos de corrotinas
Para ver mais recursos de corrotinas, consulte os seguintes links: