Kotlinautas
Esse conteúdo é oferecido e distribuído pela comunidade Kotlinautas, uma comunidade brasileira que busca oferecer conteúdo gratuito sobre a linguagem Kotlin em um espaço plural.
O quê são corotinas?
Corotinas (ou Coroutines) são um bloco de código que rodam concorrentemente com o resto do código, isso significa que podemos rodar dois blocos de código ao mesmo tempo, podendo assim ao mesmo tempo ler quanto enviar para um servidor por exemplo. Vamos ver mais sobre corotinas na prática durante o artigo.
Materiais
Será necessário ter o IntelliJ instalado na máquina e um conhecimento básico sobre a linguagem Kotlin.
Criando um projeto com Corotinas
Abra seu IntelliJ no menu inicial e clique em New Project:
Depois, selecione a opção Kotlin DSL build script, selecione também a opção Kotlin/JVM, e opicionalmente remova a primeira opção Java. Essa opção não vai mudar em nada, pois ela dá suporte do Gradle á linguagem Java, mas apenas iremos usar Kotlin.
Após isso, clique em Next e escreva o nome do projeto e a localização na sua máquina. Essas duas opção são completamente pessoais, caso não tenha nenhuma ideia, coloque algo como Corotinas apenas como identificação.
Agora, com o projeto aberto, vá ao aquivo build.gradle.kts
e adicione a dependência implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1")
, com a seção dependencies
ficando assim:
dependencies {
implementation(kotlin("stdlib"))
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.5.1")
}
Agora, clique no elefante no canto superior direito para carregar as alterações no Gradle.
Após isso, poderemos começar a programar. Você pode criar um arquivo em src/main/kotlin/
chamado main.kt
para ser o arquivo principal da aplicação.
Mas com qualquer nome de arquivo, como você irá usar as corotinas, sempre se lembre de importar a biblioteca de corotinas no começo do arquivo:
import kotlinx.coroutines.*
Primeira Corotina
Vamos criar o primeiro exemplo, vamos criar uma corotina que irá rodar paralelamente com o código principal, o código principal apenas irá mostrar um "Olá", enquanto o código da corotina irá esperar um segundo, e após isso, irá mostrar um "Mundo!". Podemos fazer isso da seguinte forma:
import kotlinx.coroutines.*
fun main() = runBlocking {
launch {
delay(1000L)
println("Mundo!")
}
println("Olá")
}
Coloque esse código no seu IntelliJ e rode. O output esperado desse código é esse:
Olá
Mundo!
Agora vamos explicar o quê esse código está fazendo:
-
RunBlocking
é um bloco que irá armazenar todas as corotinas de uma parte do código, como se criasse um contexto diferente do normal damain
. Todas as corotinas devem estar dentro de um blocorunBlocking
; -
launch
irá iniciar uma corotina, que irá funcionar concorrentemente (ao mesmo tempo) e independente do resto do código, podemos inserir quantos blocoslaunch
que quisermos dentro de um mesmo código; -
delay
é uma função que faz a corotina esperar por um tempo em milisegundos, e voltar com o processamento após esse tempo. Essa função recebe um número do tipoLong
, que pode ser criado colocando umL
no final de um número;
O runBlocking
guarda um launch
dentro, iniciando uma nova corotina, que a primeira instrução é o delay(1000L)
, fazendo que a corotina espere por um segundo (1000 milisegundos), enquanto isso o código principal continua, mandando um Olá
na tela. E após um segundo da corotina rodando, a proxima e ultima instrução manda um Mundo!
na tela.
Refatorando para uma função
Agora vamos transformar o conteúdo de dentro do bloco launch
em uma função. Para isso, iremos precisar usar um suspend
antes da função. (Função de suspensão)
Mas, o quê é esse
suspend
?
funções com suspend
são funções que podem ser usadas normalmente dentro de corotinas, mas podem usar algumas funções especiais, como a função delay
que como foi explicado mais cedo, serve para fazer a corotina esperar um tempo em milisegundos.
Com isso em mente, vamos criar a função:
suspend fun escreverMundo() {
delay(1000L)
println("Mundo!")
}
E agora na main
, vamos tirar tudo de dentro do bloco launch
e rodar a função escreverMundo()
dentro:
fun main() = runBlocking {
launch { escreverMundo() }
println("Olá")
}
Pronto! Agora nosso código está mais organizado, diminuindo o código da função main
.
Escopo de Corotinas
Podemos também criar um escopo onde iremos armazenar corotinas dentro. Esse escopo se chama coroutineScope
. Esse bloco é muito parecido com o bloco runBlocking
, mas tem uma diferença, enquanto o runBlocking
bloqueia a thread em uso enquanto está esperando algo, o coroutineScope
libera a thread para outros usos enquanto espera algo.
Como o
coroutineScope
consegue fazer isso?
Porque o coroutineScope
é uma função de suspensão, enquanto o runBlocking
é uma função normal. Por isso coroutineScope
tem essas habilidades especiais.
Agora, vamos mudar a função escreverMundo
, para fazer que essa função use os poderes de um coroutineScope
:
suspend fun escreverMundo() = coroutineScope {
launch {
delay(1000L)
println("Mundo!")
}
launch{
delay(4000L)
println("Já se passaram 4 segundos né?")
}
println("Olá")
}
- Agora, a função
escreverMundo
recebe umacoroutineScope
; - Como uma
coroutineScope
, podemos colocar vários blocoslaunch
dentro. No caso, há dois blocos; - O primeiro bloco, espera por um segundo e depois escreve um
Mundo!
na tela; - O segundo bloco espera por 4 segundos, e depois escreve na tela
Já se passaram 4 segundos né?
; - E abaixo destes dois blocos, há a instrução para escrever um
Olá
na tela.
Por conta que essas três partes serão executadas ao mesmo tempo, primeiro irá aparecer Olá
, depois de um segundo Mundo!
, e depois de 4 segundos que o programa começou a rodar, irá aparecer o Já se passaram 4 segundos né?
.
Mas para que esse código rode corretamente, também precisamos mudar a função main
adaptando para que possamos usar a função escreverMundo
como coroutineScope
fun main() = runBlocking {
escreverMundo()
}
Agora, removemos o launch
pois ele irá impedir que a main
rode corretamente.
O resultado esperado do programa agora é:
Olá
Mundo!
Já se passaram 4 segundos né?
Agora vamos fazer uma experiência, vamos remover o println("Olá")
na função escreverMundo
, e vamos colocar no final da função main
, dessa maneira:
import kotlinx.coroutines.*
fun main() = runBlocking {
escreverMundo()
println("Olá")
}
suspend fun escreverMundo() = coroutineScope {
launch {
delay(1000L)
println("Mundo!")
}
launch{
delay(4000L)
println("Já se passaram 4 segundos né?")
}
}
O resultado desse código é:
Mundo!
Já se passaram 4 segundos né?
Olá
- Como a função
runBlocking
bloqueia a thread enquanto está rodando, primeiro, todas as instruções deescreverMundo
são rodadas, e após isso que o código irá continuar, mandando oOlá
na tela.
Com todos esses recursos, dá pra fazer bastante coisa usando escopos de corotinas com coroutineScope
, iniciar partes do código com corotinas com runBlocking
, iniciar uma corotina com launch
, e fazer uma corotina esperar um tempo com delay
.
Jobs (Tarefas)
Jobs ou tarefas são instâncias de corotinas, que podem ser manipuladas para por exemplo, cancelar a corotina, esperar a corotina terminar todo o processamento para que o código principal continue,etc. Vamos ver esse exemplo abaixo:
import kotlinx.coroutines.*
fun main() = runBlocking {
val tarefa = launch {
delay(1000L)
println("Mundo!")
}
println("Olá")
tarefa.join()
println("Fim")
}
-
main
recebe um blocorunBlocking
, podendo assim usar as corotinas dentro; - é criada uma variável chamada
tarefa
que recebe uma corotina em um blocolaunch
. Com isso, a corotina é iniciada e o código principal continua; - Após isso, é escrito na tela um
Olá
; - A função
tarefa.join()
faz com que a corotinatarefa
tenha de terminar para que o código principal continue, com isso a instruçãoprintln("Fim")
apenas irá rodar depois da corotinatarefa
- Após isso, a corotina espera um segundo, com a instrução
delay(1000L)
; - E ao final da corotina
tarefa
, é escrito umMundo!
na tela; - E depois da corotina
tarefa
ter acabado, é escrito umFim
na tela.
Com isso em mente, o output esperado é
Olá
Mundo!
Fim
Mas, e se eu quiser que a corotina
tarefa
rode junto com o código da funçãomain
?
Podemos fazer isso mudando na linha 9 de tarefa.join()
para tarefa.start()
, com isso o nosso código ficará assim:
import kotlinx.coroutines.*
fun main() = runBlocking {
val tarefa = launch {
delay(1000L)
println("Mundo!")
}
println("Olá")
tarefa.start()
println("Fim")
}
O output esperado dessa maneira é:
Olá
Fim
Mundo!
Isso acontece pois enquanto a função tarefa.join()
suspende a thread (main
no caso) enquanto roda, a função tarefa.start()
apenas inicia uma corotina (no caso a corotina tarefa
), e continua a rodar o código principal.
Cancelando tarefas
Agora vamos aprender a como cancelar uma tarefa, esse conhecimento é útil para aplicações que irão rodar por muito tempo sem parar, e vão precisar iniciar e fechar corotinas constantemente, como por exemplo, uma aplicação web feita em Ktor. (Caso você tenha interesse em Ktor, leia esse artigo da Kotlinautas Criando uma API com Ktor)
Primeiro, vamos criar uma main
que recebe um runBlocking
:
import kotlinx.coroutines.*
fun main() = runBlocking{
}
Agora, vamos criar uma variável tarefa
que recebe um launch
:
fun main() = runBlocking{
val tarefa = launch {
repeat(1000) { i ->
println("tarefa: Estou rodando fazem $i vezes")
delay(500L)
}
}
}
- A variável
tarefa
recebe umlaunch
, logo sendo uma corotina; - Dentro da corotina, há um
repeat(1000)
, esserepeat
inicia um código que irá rodar por um número determinado de vezes, no caso, 1000 vezes; - E dentro desse bloco, é mostrado na tela um texto
tarefa: Estou rodando fazem $i vezes
, sendo$i
o número de vezes que orepeat
já repetiu; - Depois desse texto ser mostrado na tela, a corotina é suspensa por 500 milesegundos (meio segundo);
Agora, vamos fazer que a main
espere um tempo, escreva na tela que não deseja mais esperar que a corotina tarefa
termine seu processamento, cancele a corotina tarefa
, e feche a main
em seguida;
import kotlinx.coroutines.*
fun main() = runBlocking{
val tarefa = launch {
repeat(1000) { i ->
println("tarefa: Estou rodando fazem $i vezes")
delay(500L)
}
}
delay(1300L)
println("main: Não quero mais esperar pela tarefa!")
tarefa.cancel()
tarefa.join()
println("main: Agora eu posso fechar")
}
- Agora, a
main
espera 1.3 segundos, e após isso, será mostrado na tela um textomain: Não quero mais esperar pela tarefa!
; - Após isso, é usada a função
tarefa.cancel()
para cancelar a corotina, fazendo a corotinatarefa
terminar; - Para fazer que o resto do código rode apenas quando a corotina for completamente cancelada, é usada a função
tarefa.join()
novamente; - Após isso, a
main
escreve na telamain: Agora eu posso fechar
O output esperado desse programa é:
tarefa: Estou rodando fazem 0 vezes
tarefa: Estou rodando fazem 1 vezes
tarefa: Estou rodando fazem 2 vezes
main: Não quero mais esperar pela tarefa!
main: Agora eu posso fechar
Segundo a própria documentação do Kotlin, a função .cancel()
cancela a tarefa (corotina sendo armazenada em uma variável), incluindo todas as corotinas iniciadas por essa.
Mas não é toda corotina que pode ser cancelada dessa maneira, vamos ver o exemplo á seguir:
import kotlinx.coroutines.*
fun main() = runBlocking{
val tarefa = launch {
while (isActive) {
println("tarefa: Estou rodando!")
delay(1000L)
}
}
delay(5000L)
println("main: Não quero mais esperar pela tarefa!")
tarefa.cancel()
tarefa.join()
println("main: Agora eu posso fechar")
}
- Agora, ao invés de um
repeat(1000)
, temos umwhile(isActive)
,isActive
é uma variável interna da corotina, que sempre é verdadeira enquanto a corotina não terminou ou não foi cancelada. Logo, quando usamostarefa.cancel()
, a variávelisActive
se torna falsa e a corotina é cancelada.
O output esperado desse programa é:
tarefa: Estou rodando!
tarefa: Estou rodando!
tarefa: Estou rodando!
tarefa: Estou rodando!
tarefa: Estou rodando!
main: Não quero mais esperar pela tarefa!
main: Agora eu posso fechar
Usando um try
e finally
dentro de uma corotina
Caso queiramos que a corotina faça algo antes de ser cancelada, podemos usar um bloco try
com o código da corotina, e depois do try
, dentro de um finally
o código que irá rodar quando a corotina for cancelada.
Vamos usar o seguinte exemplo:
import kotlinx.coroutines.*
fun main() = runBlocking{
val tarefa = launch {
try {
var i = 0
while (isActive) {
println("tarefa: Estou rodando fazem $i vezes")
delay(1000L)
i++
}
}finally {
println("tarefa: terminando corotina tarefa")
}
}
delay(5000L)
println("main: Não quero mais esperar pela tarefa!")
tarefa.cancel()
tarefa.join()
println("main: Agora eu posso fechar")
}
- Agora, todo o código da corotina
tarefa
está dentro de umtry
, que é o mesmo código do exemplo anterior sobreisActive
, mas agora, após otry
, dentro de umfinally
, mostramos na telatarefa: terminando corotina tarefa
, mostrando esse conceito;
O output do programa é:
tarefa: Estou rodando fazem 0 vezes
tarefa: Estou rodando fazem 1 vezes
tarefa: Estou rodando fazem 2 vezes
tarefa: Estou rodando fazem 3 vezes
tarefa: Estou rodando fazem 4 vezes
main: Não quero mais esperar pela tarefa!
tarefa: terminando corotina tarefa
main: Agora eu posso fechar
as linha 18 e 19 podem ser refatoradas em uma só, pois há o método cancelAndJoin()
, que cancela a corotina e espera pelo seu fechamento. Com isso, o nosso código ficará assim:
import kotlinx.coroutines.*
fun main() = runBlocking{
val tarefa = launch {
try {
var i = 0
while (isActive) {
println("tarefa: Estou rodando fazem $i vezes")
delay(1000L)
i++
}
}finally {
println("tarefa: terminando corotina tarefa")
}
}
delay(5000L)
println("main: Não quero mais esperar pela tarefa!")
tarefa.cancelAndJoin()
println("main: Agora eu posso fechar")
}
Timeout
É possível de criar corotinas com tempo máximo de existência, isso pode ser feito com withTimeout
, informando um tempo do tipo Long
, vamos supor o seguinte código:
import kotlinx.coroutines.*
fun main() = runBlocking{
withTimeout(1300L) {
repeat(1000) { i ->
println("Estou dormindo há $i ...")
delay(500L)
}
}
}
Caso você tente rodar esse código, irá resultar neste erro:
I'm sleeping 0 ...
I'm sleeping 1 ...
I'm sleeping 2 ...
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1300 ms
at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:186)
at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:156)
at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.common.kt:497)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:274)
at kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:69)
at java.base/java.lang.Thread.run(Thread.java:829)
Process finished with exit code 1
Nesse código, é usada a função withTimeout
, que deixa fixo o tempo que uma corotina pode rodar. Caso esse tempo passe, é retornado um erro, sendo kotlinx.coroutines.TimeoutCancellationException
.
Caso você queria que esse timeout não resulte em um erro, é possível se se usar a função withTimeoutOrNull
, dessa maneira:
import kotlinx.coroutines.*
fun main() = runBlocking{
val resultado = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("Estou dormindo $i ...")
delay(500L)
}
"Feito"
}
println("Resultado é $resultado")
}
Com isso, caso esse timeout resulte em um erro, a variável resultado
receberá o valor null
, mas caso deletemos a linha 7, que é uma espera na corotina que aumenta elevadamente o tempo de processamento, ultrapassando o valor determinado de 1.3 segundos pelo withTimeoutOrNull
o valor de resultado
será Feito
pois a corotina rodou sem problema nenhum. Dessa maneira, o código ficará assim:
import kotlinx.coroutines.*
fun main() = runBlocking{
val resultado = withTimeoutOrNull(1300L) {
repeat(1000) { i ->
println("Estou dormindo $i ...")
}
"Feito"
}
println("Resultado é $resultado")
}
Explorando mais sobre funções de suspensão
Vamos supor que temos duas funções, uma que retorna o número 10
, e outra que retorna o número 20
, e essas duas funções esperam por um segundo usando a função delay
. Por conta dessas funções terem que pausar a sua execução, terão que ser funções de suspensão, tendo um suspend
na frente. Dessa maneira:
suspend fun funçãoNúmeroUm(): Int {
delay(1000L)
return 10
}
suspend fun funçãoNúmeroDois(): Int {
delay(1000L)
return 20
}
Agora vamos criar uma main
, que irá medir o tempo de execução total do código, criar duas variáveis, cada uma sendo o retorno dessas duas funções, e mostrar o resultado dessa maneira:
import kotlin.system.measureTimeMillis
fun main() = runBlocking {
val tempo = measureTimeMillis {
val um = funçãoNúmeroUm()
val dois = funçãoNúmeroDois()
println("A soma é ${um + dois}")
}
println("Feito em $tempo milisegundos")
}
-
import kotlin.system.measureTimeMillis
importa a função que irá medir o tempo do código; - O retorno das duas funções criadas anteriormente são armazenadas nas variáveis
um
edois
; - A soma dessas duas variáveis é mostrada na tela;
- O tempo total dessas operações é guardado na variável
tempo
; - E o valor dessa variável
tempo
é mostrada na tela;
O output desse código será algo parecido com isso:
A soma é 30
Feito em 2008 milisegundos
E se eu quiser rodar essas duas funções ao mesmo tempo, economizando tempo de processamento?
Isso pode ser feito usando a função async
. A função async
inicia uma corotina como a função launch
, mas que pode receber um valor como retorno. Por isso é interessante usar async
nesses casos, pois poderemos guardar o retorno de funções de suspensão dentro de variáveis.
Vamos ver como a nossa função main
ficará com a função async
:
fun main() = runBlocking {
val tempo = measureTimeMillis {
val um = async { funçãoNúmeroUm() }
val dois = async { funçãoNúmeroDois() }
println("A soma é ${um.await() + dois.await()}")
}
println("Feito em $tempo milisegundos")
}
- Agora, as funções
funçãoNúmeroUm
efunçãoNúmeroDois
estão dentro deasync
, instânciando uma nova corotina (tarefa) para cada função; - Para pegar o valor de
um
edois
, é usada a função.await()
, que pega o resultado de dentro da corotina;
Agora, o código roda na metade do tempo pois as duas funções estão rodando ao mesmo tempo:
A soma é 30
Feito em 1015 milisegundos
Estruturando concorrências com async
Podemos melhorar ainda mais o código acima, estruturando essa concorrência em uma função, dessa maneira:
suspend fun soma(): Int = coroutineScope {
val um = async { funçãoNúmeroUm() }
val dois = async { funçãoNúmeroDois() }
um.await() + dois.await()
}
- Criamos uma função
soma
que é umcoroutineScope
, esse escopo é muito interessante de ser usado nesse tipo de caso pois se uma corotina de dentro desse escopo falhar, todas as outras também irão falhar. No caso, as duas corotinas precisam dar um resultado válido para a função retornar o número coretamente. - E o retorno da função pega o valor das variáveis
um
edois
, e soma, retornando o resultado esperado de30
.
Agora também podemos mudar a função main
para usar a função soma
:
fun main() = runBlocking {
val tempo = measureTimeMillis {
println("A soma é ${soma()}")
}
println("Feito em $tempo milisegundos")
}
Agora temos um código mais bem estruturado, seguro, e com seu output igual ainda:
A soma é 30
Feito em 1016 milisegundos
E se alguma corotina der um erro, como posso tratar esse erro usando
coroutineScope
?
Vamos mudar a funçãoNúmeroDois
para que essa função obrigatoriamente retorne um erro, dessa maneira:
suspend fun funçãoNúmeroDois(): Int {
delay(1000L)
return throw Exception("Função com erro esperado")
}
- Dessa maneira, obrigatoriamente, a
funçãoNúmeroDois
retorna um erro do tipoFunção com erro esperado
Caso você tente rodar o código dessa maneira, dará um erro por conta da funçãoNúmeroDois
:
Exception in thread "main" java.lang.Exception: Função com erro esperado
Para resolver isso, pode ser usado com bloco try
com um catch
, dessa maneira, tratando o erro. Vamos mudar a função main
mas tratando o erro:
fun main() = runBlocking {
try {
val tempo = measureTimeMillis {
println("A soma é ${soma()}")
}
println("Feito em $tempo milisegundos")
}catch(erro: Exception){
println("Ocorreu um erro: $erro")
}
}
Agora, o output do programa é:
Ocorreu um erro: java.lang.Exception: Função com erro esperado
Mesmo que o erro Função com erro esperado
tenha acontecido, a main
fechou sem problemas, pois os blocos try
e catch
trataram o erro.
Finalização
Esse é o básico sobre corotinas no Kotlin. Há muito mais detalhes e conteúdos que podem ser abordados, mas para um artigo introdutório isso já é suficiente.
Muito obrigada por ler ❤️🏳️⚧️ e me segue nas redes, é tudo @lissatransborda 👀
Top comments (0)