2

Uma introdução às coroutines no Android com Kotlin

#Kotlin #Android
Francisco Rasia
Francisco Rasia

Dizem que existe um monstro nas entranhas do Android. Ele fica à espreita, aguardando aplicativos que rodam lentamente. Dizem que se o app ficar mais de dez segundos sem responder ele ataca e arrasta a vítima para as profundezas do garbage collector. Esse terrível monstro devorador de apps é conhecido por uma sigla: ANR, application not responding.

É sabido que somente os programadores e programadoras mais valentes, que ousam adentrar o mundo das threads paralelas e das corrotinas sabem como combater essa terrível criatura.

Se você, como eu, achava que teria que descobrir segredos arcanos do universo Android para desvendar os mistérios das corrotinas, tenho uma ótima notícia: aplicar a API de corrotinas é mais fácil do que parece.

Nesse artigo vou mostrar como, conhecendo alguns conceitos simples, é possível delegar operações demoradas para funções suspend executadas dentro do escopo de corrotinas, simplificando (e muito) o gerenciamento de threads e tarefas no Android, e driblando o terrível Monstro ANR!​ ​ 👿

Mas, afinal, o que são essas famosas corrotinas? Nas palavras da documentação oficial, "Corrotinas são um recurso do Kotlin que converte callbacks assíncronos das tarefas demoradas, tais como o acesso à rede ou bases de dados, em código sequencial". Trocando em miúdos, as corrotinas simplificam - e muito - o gerenciamento de tarefas assíncronas e de seus múltiplos callbacks, resultando em um código limpo e fácil de interpretar, como vamos trabalhar na parte prática desse artigo.


⏳O que é uma função suspend?

A palavra chave suspend é a maneira que o Kotlin utiliza para marcar uma função ou um tipo de função como disponível para corrotinas. Quando uma corrotina chama uma função demorada (por exemplo, uma consulta ao banco de dados) marcada com suspend, ao invés de bloquear a thread principal e, conseguintemente, a interface do usuário, a função é colocada em um estado de suspensão até receber o resultado, que é então retornado para thread principal.

Ao invés de se abrir uma thread para cada uma, as corrotinas rodam em um pool de threads compartilhadas - e podem até mesmo ser executadas na thread principal.

Todas as corrotinas em Kotlin rodam dentro de um CoroutineScope, que controla o ciclo de vida das corrotinas durante a tarefa. Se a tarefa do escopo for cancelada, por exemplo, fechando a janela do aplicativo antes de sua conclusão, todas as corrotinas iniciadas são canceladas.

No Android comumente encontramos corrotinas iniciadas dentro de um LifeCycleScope ou de um ViewModelScope:

  • LifeCycleScope: um escopo definido para cada objeto que possui um ciclo de vida associado, tais como fragments e activities; qualquer corrotina iniciada aqui será cancelada quando o objeto for destruído.
  • ViewModelScope: um escopo associado a cada objeto do tipo ViewModel no app; qualquer corrotina iniciada nesse escopo será cancelada quando o ViewModel for eliminado.

Existe ainda o GlobalScope, que opera no nível da aplicação e não pode ser cancelado prematuramente. Entretanto, não se encoraja lançar corrotinas nesse escopo e se deve dar preferência ao LifeCycleScope e ViewModelScope.


🚀 Botando a mão na massa

Vamos iniciar com um projeto simples em Android, uma tela com dois botões e dois contadores. Ao clicar em um deles, o contador é incrementado em 1 unidade após uma pausa de 1 segundo; o clique no outro botão incrementa o contador em 5 unidades após uma pausa de 5 segundos.


Você pode baixar o app no repositório: https://github.com/chicorasia/bootcamp-coroutines-playground


override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 binding = ActivityMainBinding.inflate(layoutInflater)
 setContentView(binding.root)
​
 timer1StartBtn.setOnClickListener {
   Thread.sleep(1000) //pause por 1s
   timer1 += 1.0
   updateTimers()
​
 }
​
 timer2StarBtn.setOnClickListener {
   Thread.sleep(5000) //pause por 5s
   timer2 += 5.0
   updateTimers()
 }
​
 resetTimerBtn.setOnClickListener {
   timer1 = 0.0
   timer2 = 0.0
   updateTimers()
 }
​
 updateTimers()
​
}

Veja que estamos pausando a thread principal com a função Thread.sleep( ), que recebe um valor em milissegundos como parâmetro. Ao chegar nessa linha, thread principal fica bloqueada pelo tempo especificado - e a UI fica congelada. Experimente rodar o app e você vai perceber que a experiência do usuário é péssima - apesar dele ainda registrar os cliques, a UI não dá nenhum feedback visual para indicar que os botões foram clicados.


🚀 Resolvendo com thread paralelas

Vamos melhorar a responsividade do app delegando as tarefas para threads secundárias. Vamos começar extraindo os blocos de código para funções à parte para ficar mais claro:

private fun incrementaApos5000Milissegundos() {
 Thread.sleep(5000)
 timer2 += 5.0
 updateTimers()
}
​
private fun incrementaApos1000Milissegundos() {
 Thread.sleep(1000)
 timer1 += 1.0
 updateTimers()
}

Podemos tentar fazer essas funções rodar em threads à parte colocando o corpo dentro de um bloco Thread { }e invocando o método start( ). Mas essa implementação já começa a gerar problemas, pois, como não é possível atualizar elementos da UI à partir da thread paralela, precisamos indicar que o métodos de atualizar os contadores seja executado na thread principal, usando runOnUiThread { }:

private fun incrementaApos5000Milissegundos() {
 Thread {
   sleep(5000)
   timer2 += 5.0
   runOnUiThread { updateTimers() }
 }.start()
}
​
private fun incrementaApos1000Milissegundos() {
 Thread {
   sleep(1000)
   timer1 += 1.0
   runOnUiThread { updateTimers() }
 }.start()
​
​
}

Apesar desse momento Inception, a experiência do usuário já é muito melhor - os cliques são processados em uma thread de fundo e a UI continua responsiva. Ufa, estamos seguros do Monstro ANR 👿, pelo menos por enquanto!


🚀 Aplicando corrotinas e funções suspend

O primeiro passo para implementar solução com corrotinas é adicionar as dependências necessárias ao arquivo build.gradle do módulo do app:

implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1'
 

Agora, podemos reescrever nossos métodos de incremento usando o modificador suspend na assinatura. Além disso, vamos tirar as referências às Threads e trocar o comando sleep( ) por delay( ), também indicando as pausas em milissegundos:

private suspend fun incrementaApos5000Milissegundos() {
 delay(5000) //aguarde 5s
 timer2 += 5.0
 updateTimers()
}
​
private suspend fun incrementaApos1000Milissegundos() {
 delay(1000) //aguarde 1s
 timer1 += 1.0
 updateTimers()
}

O modificador suspend indica que a execução daquela função pode ser suspensa, liberando a thread principal. Mas, para iniciar esses métodos, é preciso indicar o escopo de execução. Vamos fazer isso no escopo da Activity usando lifeCycleScope.launch { } em nossos listeners de clique:

timer1StartBtn.setOnClickListener {
 lifecycleScope.launch { incrementaApos1000Milissegundos() }
}
​
timer2StarBtn.setOnClickListener {
 lifecycleScope.launch { incrementaApos5000Milissegundos() }
}

E pronto! Tente rodar novamente e veja que os métodos de incremento não bloqueiam a UI. Com um clique no reset os contadores são zerados instantaneamente enquanto os métodos de incremento ficam suspensos.

Repare ainda que, usando a API de corrotinas, não precisamos mais nos preocupar com que funções rodam em thread paralela e quais precisam rodar na thread principal - a própria API se encarrega de gerenciar as tarefas.


✅Conclusão

Nesse artigo nós vimos os fundamentos do uso de corrotinas e funções suspend no Android com Kotlin. Vimos que o primeiro passo é adicionar o modificador suspend na assinatura do método, que depois pode ser invocado dentro do escopo de uma corrotina usando LifeCycleScope (se estiver em um fragmento ou uma activity) ou com o ViewModelScope (se estiver em um ViewModel).

Há muito mais para se descobrir nessa API: por exemplo, como fazer chamadas de métodos que bloqueiam a UI de forma segura usando os Dispatchers, aproveitar as facilidades das corrotinas com outros elementos da arquitetura Android (por exemplo, Room e Retrofit), e assim por diante. ​​

Mas de uma coisa você pode ter certeza: você acaba de entrar na liga dos bravos programadores e indômitas programadoras que não têm mais motivos para temer o Monstro ANR 👿 !


🔎Para saber mais

🏭 Codelabs: https://developer.android.com/codelabs/kotlin-coroutines#0

📖Documentação: https://developer.android.com/topic/libraries/architecture/coroutines

💻 Repositório no github: https://github.com/chicorasia/bootcamp-coroutines-playground

💊 Pílula de código - introdução às corrotinas no Android com Kotlin: https://youtu.be/jPY4UD1bptQ


📷

Erik Mclean on Unsplash

3
25

Comentários (4)

0
Eric Kenzo

Eric Kenzo

14/04/2021 16:09

Muito bom o artigo! top demais

0
Ana Guerra

Ana Guerra

13/04/2021 19:26

O francisco, vou te falar em! Só assunto de qualidade, deveria ter uma forma de armazenar os artigos favoritos aqui na plataforma... Se tivesse, eu já teria todos os seus! kkkkk Parabéns pelos temas, muito bons.

0
Sérgio Cabral

Sérgio Cabral

13/04/2021 19:04

Muito bom esse artigo e meus parabéns pela iniciativa !! Ficou demais!!

0
Leonardo Sena

Leonardo Sena

13/04/2021 18:42

mlk brabo.

Arquiteto, urbanista, desenvolvedor Java & Android e criador em chefe na chicorialabs.com.br

Brasil