Introdução a corrotinas no Playground Kotlin

1. Antes de começar

Este codelab apresenta a simultaneidade, uma habilidade essencial para que os desenvolvedores Android entendam e ofereçam uma ótima experiência do usuário. A simultaneidade envolve a execução de várias tarefas no app ao mesmo tempo. Por exemplo, o app pode receber dados de um servidor da Web ou salvar dados do usuário no dispositivo, respondendo a eventos de entrada do usuário e atualizando a IU de acordo com isso.

Para usar a simultaneidade no seu app, você vai usar as corrotinas do Kotlin. As corrotinas permitem que a execução de um bloco de código seja suspensa e retomada mais tarde para que outros processos possam ser feitos nesse meio tempo. As corrotinas facilitam a criação de códigos assíncronos. Isso significa que uma tarefa não precisa ser concluída para o início de outra, permitindo a execução simultânea de várias delas.

Este codelab oferece alguns exemplos básicos no Playground Kotlin, onde você tem experiência prática com corrotinas para se acostumar com a programação assíncrona.

Pré-requisitos

  • Saber criar um programa básico em Kotlin com uma função main().
  • Conhecimento básico de Kotlin, incluindo funções e lambdas.

O que você vai criar

  • Um programa Kotlin curto para aprender e testar os conceitos básicos das corrotinas.

O que você vai aprender

  • Como as corrotinas do Kotlin podem simplificar a programação assíncrona.
  • A finalidade da simultaneidade estruturada e por que ela é importante.

O que é necessário

2. Código síncrono

Programa simples

No código síncrono, apenas uma tarefa conceitual é realizada por vez. Pense nela como um caminho linear sequencial. Uma tarefa precisa ser completamente concluída antes do início da próxima. Confira abaixo um exemplo de código síncrono.

  1. Abra o Playground Kotlin.
  2. Substitua o código existente pelo exemplo abaixo, um programa que mostra a previsão do tempo como "ensolarado". Na função main(), primeiro vamos mostrar o texto: Weather forecast. Depois, vamos mostrar: Sunny.
fun main() {
    println("Weather forecast")
    println("Sunny")
}
  1. Execute o código. A saída da execução do código acima será:
Weather forecast
Sunny

println() é uma chamada síncrona, porque a tarefa de mostrar o texto na saída é concluída antes da execução ir para a próxima linha de código. Como cada chamada de função no main() é síncrona, toda a função main() é síncrona. As partes da função determinam se ela é síncrona ou assíncrona.

Uma função síncrona retorna apenas quando a tarefa está totalmente concluída. Quando a última instrução de exibição no main() for executada, todo o trabalho estará concluído. A função main() retorna e o programa termina.

Adicionar um atraso

Agora vamos supor que para receber a previsão de tempo ensolarado seja necessária uma solicitação de rede a um servidor da Web remoto. Simule a solicitação de rede adicionando um atraso no código antes de mostrar a previsão do tempo.

  1. Primeiro, adicione import kotlinx.coroutines.* na parte de cima do código antes da função main(). Isso importa as funções que você vai usar da biblioteca de corrotinas do Kotlin.
  2. Modifique seu código para adicionar uma chamada para delay(1000), que atrasa a execução do restante da função main() em 1000 milissegundos, ou um segundo. Insira esta chamada delay() antes da instrução de exibição para Sunny.
import kotlinx.coroutines.*

fun main() {
    println("Weather forecast")
    delay(1000)
    println("Sunny")
}

Na verdade, o delay() é uma função de suspensão especial fornecida pela biblioteca de corrotinas do Kotlin. A execução da função main() será suspensa (ou pausada) neste estado e será retomada quando a duração especificada do atraso for atingida (um segundo neste caso).

Se você tentar executar o programa nesse momento, vai ocorrer um erro de compilação: Suspend function 'delay' should be called only from a coroutine or another suspend function.

Para aprender a usar corrotinas no Playground Kotlin, é possível unir o código existente com uma chamada para a função runBlocking() da biblioteca de corrotinas. runBlocking() executa uma repetição de eventos, que pode processar várias tarefas de uma vez ao continuar cada tarefa de onde parou quando o programa estiver pronto para ser retomado.

  1. Mova o conteúdo da função main() para o corpo da chamada runBlocking {}. O corpo de runBlocking{} é executado em uma nova corrotina.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        delay(1000)
        println("Sunny")
    }
}

A chamada runBlocking() é síncrona. Ela não vai retornar até que todo o trabalho dentro do bloco lambda esteja concluído. Isso significa que ele vai aguardar a conclusão do trabalho na chamada de delay() (até um segundo de intervalo), para então continuar executando a instrução de exibição Sunny. Depois que todo o trabalho na função runBlocking() for concluído, a função vai retornar e o programa será encerrado.

  1. Execute o programa. Esta é a saída:
Weather forecast
Sunny

A saída é a mesma de antes. O código ainda é síncrono: é executado em uma linha reta e realiza apenas uma tarefa por vez. No entanto, a diferença é que agora ele é executado por um período mais longo devido ao atraso.

O "co-" de corrotina significa que ela é cooperativa. O código coopera para compartilhar a repetição de eventos quando ele é suspenso para aguardar algo, o que permite que outros trabalhos sejam executados nesse meio tempo. A parte "-rotina" de "corrotina" significa um conjunto de instruções, como uma função. No caso deste exemplo, a corrotina é suspensa quando chega à chamada delay(). Outra tarefa pode ser feita nesse segundo, quando a corrotina é suspensa (mesmo que neste programa não haja outra tarefa a se fazer). Quando o atraso termina, a corrotina retoma a execução e então avançar e mostrar Sunny na saída.

Funções de suspensão

Se a lógica real para realizar a solicitação de rede e conseguir os dados meteorológicos ficar mais complexa, talvez seja preciso extrair essa lógica para a própria função. Vamos refatorar o código para conferir o efeito dele.

  1. Extraia o código que simula a solicitação de rede para os dados meteorológicos e mova-o para uma função própria com o nome printForecast(). Chame printForecast() pelo código runBlocking().
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        printForecast()
    }
}

fun printForecast() {
    delay(1000)
    println("Sunny")
}

Se você executar o programa agora, o mesmo erro de compilação vai aparecer. Uma função de suspensão só pode ser chamada em uma corrotina ou outra função de suspensão. Portanto, defina printForecast() como uma função suspend.

  1. Adicione o modificador suspend logo antes da palavra-chave fun na declaração da função printForecast(). Com isso, ela se torna uma função de suspensão.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        printForecast()
    }
}

suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

Não se esqueça de que delay() é uma função de suspensão, e agora você transformou printForecast() em uma função desse tipo.

Uma função de suspensão é como uma função normal, mas pode ser suspensa e retomada novamente mais tarde. Para fazer isso, as funções de suspensão só podem ser chamadas usando outras funções desse tipo que disponibilizem esse recurso.

Uma função de suspensão pode conter zero ou mais pontos de suspensão. Um ponto de suspensão é o lugar dentro da função em que a execução da função pode ser suspensa. Quando a execução é retomada, ela retoma de onde parou no código e continua com o restante da função.

  1. Pratique adicionando outra função de suspensão ao código, abaixo da declaração da função printForecast(). Chame esta nova função de suspensão printTemperature(). Imagine que isso faz uma solicitação de rede para fornecer os dados de temperatura para a previsão do tempo.

Na função, atrase a execução em 1000 milissegundos também e então, peça a exibição de um valor de temperatura na saída, como 30 graus Celsius. É possível usar a sequência de escape "\u00b0" para mostrar o símbolo de grau, °.

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}
  1. Chame a nova função printTemperature() do código runBlocking() na função main(). Confira o código completo:
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        printForecast()
        printTemperature()
    }
}

suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}
  1. Execute o programa. A saída vai ser:
Weather forecast
Sunny
30°C

Neste código, a corrotina é suspensa pela primeira vez com o atraso na função de suspensão printForecast() e, em seguida, é retomada após esse atraso de um segundo. O texto Sunny é mostrado na saída. A função printForecast() retorna ao autor da chamada.

Em seguida, a função printTemperature() é chamada. Essa corrotina é suspensa quando atinge a chamada delay(), é retomada um segundo depois e termina mostrando o valor da temperatura na saída. A função printTemperature() concluiu todo o trabalho e retorna.

No corpo runBlocking(), não há mais tarefas a serem executadas. Portanto, a função runBlocking() retorna e o programa termina.

Como mencionado anteriormente, runBlocking() é síncrono, e cada chamada no corpo vai ocorrer em sequência. Uma função de suspensão bem projetada retorna somente depois que todas as tarefas são concluídas. Como resultado, essas funções de suspensão são executadas uma após a outra.

  1. (Opcional) Se você quiser conferir quanto tempo leva para executar esse programa com os atrasos, una o código em uma chamada para measureTimeMillis(), que vai retornar o tempo necessário em milissegundos para executar o bloco de código transmitido. Adicione a instrução de importação (import kotlin.system.*) para ter acesso a esta função. Mostre o tempo de execução e divida por 1000.0 para converter milissegundos em segundos.
import kotlin.system.*
import kotlinx.coroutines.*

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            println("Weather forecast")
            printForecast()
            printTemperature()
        }
    }
    println("Execution time: ${time / 1000.0} seconds")
}
suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}

Resultado:

Weather forecast
Sunny
30°C
Execution time: 2.128 seconds

A saída mostra que a execução levou cerca de 2,1 segundos. O tempo de execução exato pode ser um pouco diferente para você. Isso parece razoável porque cada uma das funções de suspensão tem um atraso de um segundo.

Até agora, você viu que o código em uma corrotina é invocado sequencialmente por padrão. Você precisa dar instruções explícitas para executar itens simultaneamente. Vamos aprender a fazer isso na próxima seção. Você vai usar a repetição de eventos cooperativos para realizar várias tarefas ao mesmo tempo, o que reduz o tempo de execução do programa.

3. Código assíncrono

launch()

Use a função launch() da biblioteca de corrotinas para iniciar uma nova corrotina. Para executar tarefas simultaneamente, adicione várias funções launch() ao seu código. Com isso, várias corrotinas são executadas ao mesmo tempo.

As corrotinas em Kotlin seguem um conceito-chave conhecido como simultaneidade estruturada (link em inglês), em que seu código é sequencial por padrão e coopera com uma repetição de eventos, a menos que você solicite explicitamente a execução simultânea (por exemplo, usando launch()). Presumimos que, se você chamar uma função, ela precisa terminar o trabalho completamente no momento em que for retornada, independente de quantas corrotinas ela possa ter usado nos detalhes de implementação. Mesmo que falhe com uma exceção, quando a exceção for gerada, não haverá mais tarefas pendentes na função. Todo o trabalho é concluído quando o fluxo de controle retorna da função, seja para gerar uma exceção ou concluir o trabalho.

  1. Comece com o código das etapas anteriores. Use a função launch() para mover cada chamada para printForecast() e printTemperature(), respectivamente, para as próprias corrotinas.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        launch {
            printForecast()
        }
        launch {
            printTemperature()
        }
    }
}

suspend fun printForecast() {
    delay(1000)
    println("Sunny")
}

suspend fun printTemperature() {
    delay(1000)
    println("30\u00b0C")
}
  1. Execute o programa. Esta é a saída:
Weather forecast
Sunny
30°C

A saída é a mesma, mas você deve ter percebido que a execução do programa é mais rápida. Anteriormente, era necessário aguardar a conclusão da função de suspensão printForecast() antes de mover para a função printTemperature(). Agora, printForecast() e printTemperature() podem ser executados simultaneamente porque estão em corrotinas separadas.

A instrução println (Weather Forecast) está em uma caixa na parte de cima do diagrama. Abaixo dela, há uma seta apontando para baixo. Fora dessa seta, há uma ramificação à direita, com uma seta apontando para uma caixa que contém a instrução printForecast(). Além dessa seta original, também há outra ramificação à direita, com uma seta apontando para uma caixa que contém a instrução printTemperature().

A chamada para launch { printForecast() } pode retornar antes de todo o trabalho em printForecast() ser concluído. Essa é a beleza das corrotinas. Você pode passar para a próxima chamada launch() para iniciar a próxima corrotina. Da mesma forma, launch { printTemperature() } também retorna antes de todo o trabalho ser concluído.

  1. (Opcional) Se você quiser conferir a velocidade do programa agora, adicione o código measureTimeMillis() para verificar o tempo de execução.
import kotlin.system.*
import kotlinx.coroutines.*

fun main() {
    val time = measureTimeMillis {
        runBlocking {
            println("Weather forecast")
            launch {
                printForecast()
            }
            launch {
                printTemperature()
            }
        }
    }
    println("Execution time: ${time / 1000.0} seconds")
}

...

Resultado:

Weather forecast
Sunny
30°C
Execution time: 1.122 seconds

O tempo de execução diminuiu de ~ 2,1 segundos para ~ 1,1 segundos, assim é mais rápido executar o programa depois de adicionar operações simultâneas. Você pode remover esse código de medição de tempo antes de seguir para as próximas etapas.

O que você acha que vai acontecer se você adicionar outra instrução de exibição após a segunda chamada de launch(), antes do fim do código runBlocking()? Onde essa mensagem apareceria na saída?

  1. Modifique o código runBlocking() para adicionar outra instrução de exibição antes do fim do bloco.
...

fun main() {
    runBlocking {
        println("Weather forecast")
        launch {
            printForecast()
        }
        launch {
            printTemperature()
        }
        println("Have a good day!")
    }
}

...
  1. Execute o programa. Esta é a saída:
Weather forecast
Have a good day!
Sunny
30°C

A partir dessa saída, é possível observar que, após a inicialização das duas novas corrotinas para printForecast() e printTemperature(), você pode seguir para a próxima instrução, que mostra Have a good day!. Isso demonstra a natureza "disparar e esquecer" de launch(). Você dispara uma nova corrotina com o launch() e não precisa se preocupar com o término do trabalho.

Mais tarde, as corrotinas vão concluir o trabalho e mostrar as outras instruções de saída. Depois que todo o trabalho (incluindo de todas as corrotinas) no corpo da chamada runBlocking() for concluído, runBlocking() vai retornar e o programa termina.

Agora você mudou seu código síncrono para um código assíncrono. Quando uma função assíncrona retorna, a tarefa pode ainda não ter sido concluída. Isso foi o que você observou no caso de launch(). A função retornou, mas o trabalho dela ainda não foi concluído. Com o uso de launch(), várias tarefas podem ser executadas simultaneamente no seu código. Esse é um recurso avançado para usar nos apps Android que você desenvolver.

async()

No mundo real, você não sabe quanto tempo as solicitações de rede para previsão e temperatura vão levar. Se você quiser mostrar um relatório meteorológico unificado quando as duas tarefas forem concluídas, a abordagem atual com launch() não é suficiente. É aí que entra o async().

Use a função async() da biblioteca de corrotinas se você quiser saber quando a corrotina termina e precisa de um valor de retorno.

A função async() retorna um objeto do tipo Deferred, que é uma promessa de que o resultado vai estar lá quando estiver pronto. É possível acessar o resultado no objeto Deferred usando await().

  1. Primeiro, mude as funções de suspensão para retornar uma String em vez de mostrar os dados de previsão e temperatura. Atualize os nomes das funções de printForecast() e printTemperature() para getForecast() e getTemperature().
...

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Modifique o código runBlocking() para que ele use async() em vez de launch() para as duas corrotinas. Armazene o valor de retorno de cada chamada async() em variáveis com os nomes forecast e temperature, que são objetos Deferred que contêm um resultado do tipo String. Especificar o tipo é opcional devido à inferência de tipo em Kotlin. No entanto, ele foi incluído abaixo para você saber o que está sendo retornado pelas chamadas async().
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        val forecast: Deferred<String> = async {
            getForecast()
        }
        val temperature: Deferred<String> = async {
            getTemperature()
        }
        ...
    }
}

...
  1. Mais tarde na corrotina, depois das duas chamadas async(), você pode acessar o resultado dessas corrotinas chamando await() nos objetos Deferred. Nesse caso, é possível mostrar o valor de cada corrotina usando forecast.await() e temperature.await().
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        val forecast: Deferred<String> = async {
            getForecast()
        }
        val temperature: Deferred<String> = async {
            getTemperature()
        }
        println("${forecast.await()} ${temperature.await()}")
        println("Have a good day!")
    }
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Execute o programa. A saída vai ser:
Weather forecast
Sunny 30°C
Have a good day!

Tudo pronto. Você criou duas corrotinas que são executadas simultaneamente para receber os dados de previsão e temperatura. Quando cada uma delas é concluída, um valor é retornado. Em seguida, você combinou os dois valores de retorno em uma única instrução de exibição: Sunny 30°C.

Decomposição paralela

Podemos avançar ainda mais neste exemplo meteorológico e conferir como as corrotinas podem ser úteis na decomposição paralela do trabalho. A decomposição paralela envolve dividir um problema em subtarefas menores que podem ser resolvidas em paralelo. Quando os resultados das subtarefas estiverem prontos, você vai poder combiná-los em um resultado final.

No código, extraia a lógica do relatório meteorológico do corpo de runBlocking() em uma única função getWeatherReport(), que retorna a string combinada de Sunny 30°C.

  1. Defina uma nova função de suspensão getWeatherReport() no código.
  2. Defina a função igual ao resultado de uma chamada para a função coroutineScope{} com um bloco lambda vazio que, por fim, vai conter a lógica para acessar o relatório meteorológico.
...

suspend fun getWeatherReport() = coroutineScope {

}

...

O coroutineScope{} cria um escopo local para essa tarefa de previsão do tempo. As corrotinas iniciadas neste escopo são agrupadas nesse escopo, o que afeta o cancelamento e as exceções que você vai conhecer em breve.

  1. No corpo do coroutineScope(), crie duas novas corrotinas usando async() para buscar os dados de previsão e temperatura, respectivamente. Crie a string do relatório meteorológico combinando esses resultados das duas corrotinas. Para fazer isso, chame await() em cada um dos objetos Deferred retornados pelas chamadas de async(). Isso garante que cada corrotina conclua o trabalho e retorne o resultado antes do retorno dessa função.
...

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

...
  1. Chame esta nova função getWeatherReport() do runBlocking(). Confira o código completo:
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Execute o programa. Você vai encontrar esta saída:
Weather forecast
Sunny 30°C
Have a good day!

A saída é a mesma, mas há algumas considerações importantes aqui. Como mencionado anteriormente, coroutineScope() só vai retornar depois que todo o trabalho for concluído, incluindo as corrotinas iniciadas. Nesse caso, as corrotinas getForecast() e getTemperature() precisam terminar e retornar os respectivos resultados. Em seguida, o texto Sunny e o 30°C são combinados e retornados do escopo. Essa previsão do tempo de Sunny 30°C é mostrada na saída, e o autor da chamada pode seguir para a última instrução de exibição de Have a good day!.

Com a coroutineScope(), mesmo que a função esteja trabalhando internamente de maneira simultânea, ela aparece para o autor da chamada como uma operação síncrona porque coroutineScope não vai retornar até que todo o trabalho seja concluído.

O principal insight para a simultaneidade estruturada é que você pode executar várias operações simultâneas e colocá-las em uma única operação síncrona, em que a simultaneidade é um detalhe de implementação. O único requisito do código de chamada é estar em uma função de suspensão ou corrotina. Além disso, a estrutura do código de chamada não precisa considerar os detalhes da simultaneidade.

4. Exceções e cancelamento

Agora vamos falar sobre algumas situações em que pode ocorrer um erro ou parte do trabalho pode ser cancelado.

Introdução às exceções

Uma exceção (link em inglês) é um evento inesperado que acontece durante a execução do código. Implemente maneiras adequadas de lidar com essas exceções, para evitar que o app falhe e afete negativamente a experiência do usuário.

Confira um exemplo de programa que termina antecipadamente com uma exceção. O objetivo do programa é calcular o número de pizzas que cada pessoa vai consumir, dividindo numberOfPizzas / numberOfPeople. Digamos que você se esqueça acidentalmente de definir o valor do numberOfPeople como um valor real.

fun main() {
    val numberOfPeople = 0
    val numberOfPizzas = 20
    println("Slices per person: ${numberOfPizzas / numberOfPeople}")
}

Quando você executa o programa, ele falha com uma exceção aritmética porque não é possível dividir um número por zero.

Exception in thread "main" java.lang.ArithmeticException: / by zero
 at FileKt.main (File.kt:4)
 at FileKt.main (File.kt:-1)
 at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0 (:-2)

Esse problema tem uma correção simples, em que você pode mudar o valor inicial de numberOfPeople para um número diferente de zero. No entanto, à medida que o código fica mais complexo, há alguns casos em que você não consegue antecipar e evitar todas as exceções.

O que acontece quando uma das corrotinas falha com uma exceção? Modifique o código do programa meteorológico para descobrir.

Exceções com corrotinas

  1. Comece pelo programa meteorológico da seção anterior.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}

Em uma das funções de suspensão, gere uma exceção intencionalmente para saber qual seria o efeito. Isso simula um erro inesperado ao buscar dados do servidor, o que é plausível.

  1. Na função getTemperature(), adicione uma linha de código que gera uma exceção. Escreva uma expressão geradora usando a palavra-chave throw no Kotlin, seguida por uma nova instância de uma exceção que se estende de Throwable (link em inglês).

Por exemplo, você pode gerar uma exceção de AssertionError e transmitir uma string de mensagem que descreve o erro em mais detalhes: throw AssertionError("Temperature is invalid"). A geração dessa exceção interrompe a execução da função getTemperature().

...

suspend fun getTemperature(): String {
    delay(500)
    throw AssertionError("Temperature is invalid")
    return "30\u00b0C"
}

Também é possível mudar o atraso de 500 milissegundos para o método getTemperature(). Assim, você saberá que a exceção vai ocorrer antes que a outra função getForecast() possa concluir o trabalho.

  1. Execute o programa para conferir o resultado.
Weather forecast
Exception in thread "main" java.lang.AssertionError: Temperature is invalid
 at FileKt.getTemperature (File.kt:24)
 at FileKt$getTemperature$1.invokeSuspend (File.kt:-1)
 at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith (ContinuationImpl.kt:33)

Para entender esse comportamento, você precisa saber que há uma relação mãe-filha entre as corrotinas. Você pode iniciar uma corrotina (conhecida como filha) a partir de outra corrotina (mãe). Ao iniciar mais corrotinas, você pode criar uma hierarquia inteira de corrotinas.

A corrotina que executa getTemperature() e a que executa o getForecast() são corrotinas filhas da mesma corrotina mãe. O comportamento que ocorre com exceções nas corrotinas é devido à simultaneidade estruturada. Quando uma das corrotinas filhas falha com uma exceção, ela é propagada para cima. A corrotina mãe é cancelada, o que, por sua vez, cancela qualquer outra corrotina filha (por exemplo, a corrotina que executa getForecast() nesse caso). Por fim, o erro é propagado para cima e o programa falha com a exceção AssertionError.

Detectar exceções

Se você sabe que certas partes do seu código podem gerar uma exceção, é possível cercar esse código com um bloco try-catch. Você pode capturar a exceção e processá-la de forma mais eficiente no app, como mostrar uma mensagem de erro ao usuário. Confira este snippet de código:

try {
    // Some code that may throw an exception
} catch (e: IllegalArgumentException) {
    // Handle exception
}

Essa abordagem também funciona para código assíncrono com corrotinas. Você ainda pode usar uma expressão try-catch para capturar e processar exceções em corrotinas. Isso acontece porque, com a simultaneidade estruturada, o código sequencial ainda é síncrono, então o bloco try-catch ainda vai funcionar da mesma forma esperada.

...

fun main() {
    runBlocking {
        ...
        try {
            ...
            throw IllegalArgumentException("No city selected")
            ...
        } catch (e: IllegalArgumentException) {
            println("Caught exception $e")
            // Handle error
        }
    }
}

...

Para se acostumar com o processamento de exceções, modifique o programa meteorológico para capturar a exceção adicionada anteriormente e mostre a exceção na saída.

  1. Na função runBlocking(), adicione um bloco try-catch ao redor do código que chama getWeatherReport(). Mostre o erro detectado e uma mensagem informando que a previsão do tempo não está disponível.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        try {
            println(getWeatherReport())
        } catch (e: AssertionError) {
            println("Caught exception in runBlocking(): $e")
            println("Report unavailable at this time")
        }
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(500)
    throw AssertionError("Temperature is invalid")
    return "30\u00b0C"
}
  1. Execute o programa. Agora, o erro será considerado normal e a execução do programa poderá ser concluída.
Weather forecast
Caught exception in runBlocking(): java.lang.AssertionError: Temperature is invalid
Report unavailable at this time
Have a good day!

Na saída, é possível observar que getTemperature() gera uma exceção. No corpo da função runBlocking(), você envolve a chamada println(getWeatherReport()) em um bloco try-catch. Você captura o tipo de exceção esperado (AssertionError no caso deste exemplo). Em seguida, a exceção aparece na saída como "Caught exception" seguida pela string da mensagem de erro. Para processar o erro, informe ao usuário que a previsão do tempo não está disponível com uma instrução println() extra: Report unavailable at this time.

Esse comportamento significa que, em caso de falha ao definir a temperatura, não haverá previsão do tempo (mesmo que uma previsão válida seja recebida).

Dependendo de como você quer que seu programa se comporte, há uma maneira alternativa de processar a exceção no programa meteorológico.

  1. Mova o tratamento de erros para que o comportamento do bloco try-catch realmente aconteça na corrotina iniciada por async() para buscar a temperatura. Dessa forma, a previsão do tempo ainda poderá aparecer, mesmo que a temperatura falhe. O código fica assim:
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async {
        try {
            getTemperature()
        } catch (e: AssertionError) {
            println("Caught exception $e")
            "{ No temperature found }"
        }
    }

    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(500)
    throw AssertionError("Temperature is invalid")
    return "30\u00b0C"
}
  1. Execute o programa.
Weather forecast
Caught exception java.lang.AssertionError: Temperature is invalid
Sunny { No temperature found }
Have a good day!

Na saída, é possível notar que a chamada de getTemperature() falhou com uma exceção, mas o código em async() conseguiu capturar essa exceção e processá-la normalmente, fazendo com que a corrotina ainda retornasse uma String informando que a temperatura não foi encontrada. A previsão do tempo ainda pode ser mostrada, com uma previsão bem-sucedida de Sunny. Na previsão do tempo, não há temperatura, mas há uma mensagem explicando que a temperatura não foi encontrada. Essa experiência é melhor para o usuário do que a falha do programa com o erro.

Uma maneira útil de pensar nessa abordagem de tratamento de erros é que async() é o produtor quando uma corrotina é iniciada com ele. await() é o consumidor porque está aguardando para acessar o resultado da corrotina. O produtor faz o trabalho e produz um resultado. O consumidor consome o resultado. Se houver uma exceção no produtor, o consumidor receberá essa exceção se não for processada, e a corrotina falhará. No entanto, se o produtor conseguir detectar e processar a exceção, o consumidor não receberá essa exceção, mas sim um resultado válido.

Confira o código getWeatherReport() novamente para referência:

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async {
        try {
            getTemperature()
        } catch (e: AssertionError) {
            println("Caught exception $e")
            "{ No temperature found }"
        }
    }

    "${forecast.await()} ${temperature.await()}"
}

Nesse caso, o produtor (async()) conseguiu capturar e processar a exceção e ainda retornar um resultado String de "{ No temperature found }". O consumidor (await()) recebe esse resultado String e não precisa saber que ocorreu uma exceção. Essa é outra opção para processar uma exceção esperada no código.

Você aprendeu que as exceções se propagam para cima na árvore das corrotinas, a menos que sejam processadas. Também é importante ter cuidado quando a exceção se propaga até a raiz da hierarquia, o que pode causar uma falha em todo o app. Saiba mais sobre o tratamento de exceções na postagem do blog Exceções em corrotinas e no artigo Tratamento de exceções de corrotinas (links em inglês).

Cancelamento

Um assunto semelhante às exceções é o cancelamento de corrotinas. Normalmente, esse cenário é causado pelo usuário quando um evento faz com que o app cancele um trabalho já iniciado.

Por exemplo, digamos que o usuário tenha selecionado uma preferência que não quer mais que os valores de temperatura apareçam no app. Ele quer saber apenas a previsão do tempo (por exemplo, Sunny), mas não a temperatura exata. Nesse caso, cancele a corrotina que está recebendo os dados de temperatura.

  1. Comece com o código inicial abaixo (sem cancelamento).
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("Weather forecast")
        println(getWeatherReport())
        println("Have a good day!")
    }
}

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }
    "${forecast.await()} ${temperature.await()}"
}

suspend fun getForecast(): String {
    delay(1000)
    return "Sunny"
}

suspend fun getTemperature(): String {
    delay(1000)
    return "30\u00b0C"
}
  1. Após algum atraso, cancele a corrotina que estava buscando as informações de temperatura para que sua previsão do tempo mostre apenas a previsão. Mude o valor de retorno do bloco coroutineScope para ser apenas a string da previsão do tempo.
...

suspend fun getWeatherReport() = coroutineScope {
    val forecast = async { getForecast() }
    val temperature = async { getTemperature() }

    delay(200)
    temperature.cancel()

    "${forecast.await()}"
}

...
  1. Execute o programa. A saída será como o exemplo abaixo. A previsão do tempo consiste apenas na previsão do tempo Sunny, mas não na temperatura porque essa corrotina foi cancelada.
Weather forecast
Sunny
Have a good day!

O que você aprendeu aqui é que uma corrotina pode ser cancelada, mas não afeta outras corrotinas no mesmo escopo, e a corrotina mãe não será cancelada.

Nesta seção, você aprendeu sobre como o cancelamento e as exceções se comportam em corrotinas e como isso está vinculado à hierarquia de corrotinas. Vamos saber mais sobre os conceitos formais por trás das corrotinas para entender como todas as partes importantes se unem.

5. Conceitos de corrotinas

Ao executar tarefas de forma assíncrona ou simultânea, há perguntas que precisam ser respondidas sobre como o trabalho será executado, por quanto tempo a corrotina precisa existir, o que acontece se ela for cancelada ou falhar com um erro e muito mais. As corrotinas seguem o princípio da simultaneidade estruturada, que exige que você responda a essas perguntas ao usar corrotinas no código com uma combinação de mecanismos.

Job

Quando você inicia uma corrotina com a função launch(), ela retorna uma instância de Job. O job contém um identificador, ou referência, para a corrotina para que você possa gerenciar o ciclo de vida.

val job = launch { ... }

O job pode ser usado para controlar o ciclo de vida ou a duração da corrotina, como cancelar a corrotina se você não precisar mais da tarefa.

job.cancel()

Com um job, é possível verificar se ele está ativo ou se foi cancelado ou concluído. O job será concluído se a corrotina e as corrotinas que ele inicializou tiverem concluído todo o trabalho. A corrotina pode ter sido concluída por um motivo diferente, como o cancelamento ou a falha com uma exceção, mas o job ainda é considerado concluído nesse momento.

Os jobs também rastreiam a relação mãe-filha entre as corrotinas.

Hierarquia de jobs

Quando uma corrotina inicia outra, o job retornado da nova corrotina é chamado de filho do job pai original.

val job = launch {
    ...

    val childJob = launch { ... }

    ...
}

Essas relações pai-filho formam uma hierarquia de jobs, onde cada job pode iniciar jobs e assim por diante.

Este diagrama mostra uma hierarquia de árvore de jobs. A raiz da hierarquia é um job pai. Ele tem três filhos: Child 1 Job, Child 2 Job e Child 3 Job. O Child 1 Job tem dois filhos: Child 1a Job e Child 1b Job. Além disso, o Child 2 Job tem um único filho: Child 2a Job. Por fim, o Child 3 Job tem dois filhos: Child 3a Job e Child 3b Job.

Essa relação pai-filho é importante porque determina um certo comportamento para o pai e o filho, e outros filhos que pertencem ao mesmo pai. Você aprendeu sobre esse comportamento nos exemplos anteriores do programa de previsão do tempo.

  • Se um job pai for cancelado, os jobs filhos também serão.
  • Quando um job filho é cancelado usando job.cancel(), ele é encerrado, mas o job pai não.
  • Se um job falhar com uma exceção, ele vai cancelar o pai com ela. Isso é conhecido como propagação do erro para cima (para o pai, o pai do pai e assim por diante). .

CoroutineScope

As corrotinas geralmente são iniciadas em um CoroutineScope. Isso garante que não haja corrotinas não gerenciadas e que sejam perdidas, o que pode desperdiçar recursos.

launch() e async() são funções de extensão (link em inglês) em CoroutineScope. Chame launch() ou async() no escopo para criar uma nova corrotina dentro dele.

Um CoroutineScope está vinculado a um ciclo de vida, que define limites de duração das corrotinas dentro desse escopo. Se um escopo for cancelado, o job dele também será e o cancelamento será propagado para os jobs filhos. Se um job filho no escopo falhar com uma exceção, outros jobs filhos serão cancelados, assim como o job pai, e a exceção será gerada novamente para o autor da chamada.

CoroutineScope no Playground Kotlin

Neste codelab, você usou runBlocking(), que fornece um CoroutineScope para o programa. Você também aprendeu a usar coroutineScope { } para criar um novo escopo na função getWeatherReport().

CoroutineScope em apps Android

O Android oferece suporte ao escopo de corrotinas em entidades que têm um ciclo de vida bem definido, como Activity (lifecycleScope) e ViewModel (viewModelScope). As corrotinas iniciadas nesses escopos vão seguir o ciclo de vida da entidade correspondente, como Activity ou ViewModel.

Por exemplo, digamos que você inicie uma corrotina em uma Activity com o escopo de corrotina fornecido: lifecycleScope. Se a atividade for destruída, o lifecycleScope será cancelado e todas as corrotinas filhas também serão canceladas automaticamente. Você só precisa decidir se você quer que a corrotina siga o ciclo de vida da Activity.

No app Android Race Tracker, você vai aprender a definir o escopo das corrotinas para o ciclo de vida de um elemento combinável.

Detalhes de implementação do CoroutineScope

Se você conferir o código-fonte para saber como CoroutineScope.kt é implementado na biblioteca de corrotinas Kotlin, vai notar que CoroutineScope é declarado como uma interface e contém um CoroutineContext como uma variável.

As funções launch() e async() criam uma nova corrotina filha nesse escopo, e o filho também herda o contexto do escopo. O que está incluído no contexto? Vamos falar sobre isso a seguir.

CoroutineContext

O CoroutineContext fornece informações sobre o contexto em que a corrotina será executada. O CoroutineContext é essencialmente um mapa que armazena elementos em que cada elemento tem uma chave exclusiva. Esses campos não são obrigatórios, mas confira alguns exemplos do que pode estar contido em um contexto:

  • name: nome da corrotina para identificá-la exclusivamente.
  • job: controla o ciclo de vida da corrotina.
  • dispatcher: agente que envia o trabalho para a linha de execução adequada.
  • exception handler: lida com as exceções geradas pelo código executado na corrotina.

Cada um dos elementos de um contexto pode ser anexado ao operador +. Por exemplo, um CoroutineContext pode ser definido desta maneira:

Job() + Dispatchers.Main + exceptionHandler

Como um nome não é fornecido, o nome de corrotina padrão é usado.

Em uma corrotina, se você iniciar uma nova, a filha vai herdar o CoroutineContext da corrotina mãe, mas vai substituir o job especificamente pela corrotina recém-criada. Também é possível substituir qualquer elemento herdado do contexto pai transmitindo argumentos para as funções launch() ou async() das partes do contexto que você quer que sejam diferentes.

scope.launch(Dispatchers.Default) {
    ...
}

Saiba mais sobre CoroutineContext e como o contexto é herdado do pai nesta videoconferência do KotlinConf (vídeo em inglês).

Mencionamos o dispatcher (agente) várias vezes. A função dele é enviar ou atribuir o trabalho a uma linha de execução. Vamos discutir as linhas de execução e os agentes em mais detalhes.

Dispatcher

As corrotinas usam agentes para determinar a linha de execução a ser usada na execução dela. Uma linha de execução pode ser iniciada, fazer algum trabalho (executar algum código) e ser encerrada quando não há mais trabalho a ser feito.

Quando um usuário inicia seu app, o sistema Android cria um novo processo e uma única linha de execução para seu app, que é conhecida como linha de execução principal. A linha de execução principal processa muitas operações importantes para o app, incluindo eventos do sistema Android, mostrando a IU na tela, processando eventos de entrada do usuário e muito mais. Como resultado, é provável que a maior parte do código que você cria para o app seja executado na linha de execução principal.

Há dois termos que você precisa entender sobre o comportamento da linha de execução do código: bloqueio e sem bloqueio. Uma função normal bloqueia a linha de execução da chamada até que o trabalho seja concluído. Isso significa que ela não vai gerar a linha de execução da chamada até que o trabalho seja concluído. Nenhum outro trabalho pode ser feito nesse meio tempo. Por outro lado, o código sem bloqueio permite que a linha de execução da chamada seja usada até que uma determinada condição seja atendida. Você pode fazer outro trabalho enquanto isso. É possível usar uma função assíncrona para realizar trabalhos sem bloqueio porque ela retorna antes de concluir o trabalho.

No caso de apps Android, só chame o código de bloqueio na linha de execução principal se ela for executada rapidamente. O objetivo é manter a linha de execução principal desbloqueada para que ela possa executar o trabalho imediatamente se um novo evento for acionado. Essa linha de execução principal é a linha de execução de interface das suas atividades e é responsável por mostrar a interface e eventos relacionados a ela. Quando há uma mudança na tela, a IU precisa ser renderizada de novo. Para algo como uma animação na tela, a IU precisa ser renderizada com frequência para que pareça uma transição suave. Se a linha de execução principal precisar executar um bloco de trabalho de longa duração, a tela não será atualizada com tanta frequência e o usuário vai notar uma transição abrupta (conhecida como "instabilidade") ou o app poderá travar ou demorar para responder.

Precisamos remover todos os itens de trabalho de longa duração da linha de execução principal e processá-los em uma linha de execução diferente. O app começa com uma única linha de execução principal, mas você pode criar várias linhas para realizar outros trabalhos. Elas são chamadas de linhas de execução de worker. Uma tarefa de longa duração pode bloquear uma linha de execução de worker por muito tempo, porque a linha de execução principal fica desbloqueada e pode responder ativamente ao usuário.

Há alguns agentes integrados fornecidos pelo Kotlin:

  • Dispatchers.Main: use esse agente para executar uma corrotina na linha de execução principal do Android. Esse agente é usado principalmente para processar atualizações e interações da IU e realizar trabalhos rápidos.
  • Dispatchers.IO: esse agente é otimizado para executar E/S de disco ou rede fora da linha de execução principal. Por exemplo, ler ou gravar arquivos e executar qualquer operação de rede.
  • Dispatchers.Default: esse agente padrão é usado ao chamar launch() e async() quando nenhum agente é especificado no contexto. Use esse agente para realizar trabalhos com uso intenso de computação fora da linha de execução principal. Por exemplo, processamento de um arquivo de imagem de bitmap.

Teste o exemplo abaixo no Playground Kotlin para entender melhor os agentes de corrotina.

  1. Substitua o código que você tiver no Playground Kotlin por este:
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        launch {
            delay(1000)
            println("10 results found.")
        }
        println("Loading...")
    }
}
  1. Agora, una o conteúdo da corrotina iniciada com uma chamada para withContext() para mudar o CoroutineContext em que a corrotina é executada, e modifique especificamente o agente. Comece a usar o Dispatchers.Default, em vez de Dispatchers.Main, que está sendo usado no momento para o restante do código de corrotina no programa.
...

fun main() {
    runBlocking {
        launch {
            withContext(Dispatchers.Default) {
                delay(1000)
                println("10 results found.")
            }
        }
        println("Loading...")
    }
}

A troca de agentes é possível porque withContext() (link em inglês) é uma função de suspensão. Ela executa o bloco de código fornecido usando um novo CoroutineContext. O novo contexto vem do job pai (o bloco launch() externo), mas substitui o agente usado no contexto pai pelo especificado aqui: Dispatchers.Default. É assim que podemos executar o trabalho com Dispatchers.Main e usar Dispatchers.Default.

  1. Execute o programa. A saída vai ser:
Loading...
10 results found.
  1. Adicione instruções de exibição para conferir em qual linha de execução você está chamando Thread.currentThread().name.
import kotlinx.coroutines.*

fun main() {
    runBlocking {
        println("${Thread.currentThread().name} - runBlocking function")
                launch {
            println("${Thread.currentThread().name} - launch function")
            withContext(Dispatchers.Default) {
                println("${Thread.currentThread().name} - withContext function")
                delay(1000)
                println("10 results found.")
            }
            println("${Thread.currentThread().name} - end of launch function")
        }
        println("Loading...")
    }
}
  1. Execute o programa. A saída vai ser:
main @coroutine#1 - runBlocking function
Loading...
main @coroutine#2 - launch function
DefaultDispatcher-worker-1 @coroutine#2 - withContext function
10 results found.
main @coroutine#2 - end of launch function

Nessa saída, é possível observar que a maior parte do código é executada em corrotinas na linha de execução principal. No entanto, para a parte do código no bloco withContext(Dispatchers.Default), que é executada em uma corrotina em uma linha de execução de worker do agente padrão (que não é a linha de execução principal). Após o retorno de withContext(), a corrotina retorna para a execução na linha de execução principal (conforme evidenciado pela instrução de saída: main @coroutine#2 - end of launch function). Esse exemplo demonstra que é possível alternar o agente modificando o contexto usado para a corrotina.

Se você tiver corrotinas iniciadas na linha de execução principal e quiser mover determinadas operações da linha de execução principal, use withContext para alternar o agente usado para esse trabalho. Escolha adequadamente entre os agentes disponíveis: Main, Default e IO, dependendo do tipo de operação. Em seguida, esse trabalho pode ser atribuído a uma linha de execução (ou a um grupo de linhas de execução, conhecido como pool de linhas de execução) designada para essa finalidade. As corrotinas podem suspender a si mesmas, e o agente também influencia como elas são retomadas.

Ao trabalhar com bibliotecas conhecidas, como Room e Retrofit (nesta unidade e na próxima), talvez não seja necessário alternar explicitamente o agente se o código da biblioteca já processar essa tarefa usando um agente de corrotina alternativo, como Dispatchers.IO.. Nesses casos, as funções suspend que essas bibliotecas revelam podem já ser seguras para a linha de execução principal e podem ser chamadas em uma corrotina em execução na linha de execução principal. A própria biblioteca vai processar a mudança do agente para um que use as linhas de execução de worker.

Agora você tem uma visão geral de alto nível das partes importantes das corrotinas e do papel que CoroutineScope, CoroutineContext, CoroutineDispatcher e Jobs desempenham no ciclo de vida e no comportamento de uma corrotina.

6. Conclusão

Parabéns pelo empenho em aprender sobre esse tema desafiador de corrotinas. Você aprendeu que as corrotinas são muito úteis porque a execução delas pode ser suspensa, liberando a linha de execução para outro trabalho, e ser retomada mais tarde. Assim, você pode executar operações simultâneas no seu código.

O código de corrotina no Kotlin segue o princípio da simultaneidade estruturada. Ele é sequencial por padrão. Por isso, é necessário informar explicitamente se você quer simultaneidade (por exemplo, usando launch() ou async()). Com a simultaneidade estruturada, é possível fazer várias operações simultâneas e colocá-las em uma única operação síncrona, em que a simultaneidade é um detalhe de implementação. O único requisito do código de chamada é estar em uma função de suspensão ou corrotina. Além disso, a estrutura do código de chamada não precisa considerar os detalhes da simultaneidade. Isso facilita a leitura e a compreensão do código assíncrono.

A simultaneidade estruturada monitora cada uma das corrotinas iniciadas no seu app e garante que elas não sejam perdidas. As corrotinas podem ter uma hierarquia: as tarefas podem iniciar subtarefas que, por sua vez, iniciam subtarefas. Os jobs mantêm a relação pai-filho entre as corrotinas e permitem controlar o ciclo de vida da corrotina.

Inicialização, conclusão, cancelamento e falha são quatro operações comuns na execução da corrotina. Para facilitar a manutenção de programas simultâneos, a simultaneidade estruturada define princípios que formam a base de como as operações comuns na hierarquia são gerenciadas:

  1. Inicialização: inicializa uma corrotina em um escopo que tenha um limite definido para a duração dela.
  2. Conclusão: o job não estará concluído até que os jobs filhos também estejam.
  3. Cancelamento: essa operação precisa ser propagada para baixo. Quando uma corrotina é cancelada, as corrotinas filhas também precisam ser.
  4. Falha: essa operação precisa ser propagada para cima. Quando uma corrotina gera uma exceção, o pai cancela todos os filhos e a si mesmo e propaga a exceção até o pai dele. Isso continua até que a falha seja detectada e processada. Isso garante que erros de código sejam relatados corretamente e que nunca sejam perdidos.

Com exercícios práticos sobre corrotinas e o entendimento dos conceitos por trás delas, você agora tem mais condições para criar códigos simultâneos no app Android. Ao usar corrotinas para programação assíncrona, seu código é mais simples de ler e entender, mais robusto em situações de cancelamento e exceções e oferece uma experiência mais otimizada e responsiva para os usuários finais.

Resumo

  • As corrotinas permitem criar um código de longa duração executado simultaneamente, sem que seja necessário aprender um novo estilo de programação. A execução de uma corrotina é sequencial por padrão.
  • As corrotinas seguem o princípio da simultaneidade estruturada, que ajuda a garantir que o trabalho não seja perdido e seja vinculado a um escopo com um determinado limite de duração. Por padrão, o código é sequencial e coopera com uma repetição de eventos, a menos que você solicite explicitamente a execução simultânea (por exemplo, usando launch() ou async()). Supõe-se que, se você chamar uma função, ela precisa terminar o trabalho completamente (a menos que falhe com uma exceção) no momento em que for retomada, independente de quantas corrotinas ela possa ter usado nos detalhes de implementação.
  • O modificador suspend é usado para marcar uma função cuja execução pode ser suspensa e retomada em um momento futuro.
  • Uma função suspend só pode ser chamada em outra função de suspensão ou em uma corrotina.
  • Você pode iniciar uma nova corrotina usando as funções de extensão launch() ou async() no CoroutineScope.
  • Os jobs desempenham um papel importante para garantir a simultaneidade estruturada, gerenciando o ciclo de vida das corrotinas e mantendo a relação pai-filho.
  • Um CoroutineScope controla o ciclo de vida das corrotinas com o job e aplica o cancelamento e outras regras aos filhos e aos filhos deles recursivamente.
  • Um CoroutineContext define o comportamento de uma corrotina e pode incluir referências a um job e a um agente de corrotina.
  • As corrotinas usam um CoroutineDispatcher para determinar as linhas de execução que serão usadas.

Saiba mais