Sean McQuillan

Siga-me

>

Abr 30, 2019 – 9 min leia-se

Isto faz parte de uma série multi-partes sobre o uso de Coroutines no Android. Este post foca em como os coroutines funcionam e que problemas eles resolvem.

Outros artigos desta série:

Kotlin coroutines introduz um novo estilo de concurrency que pode ser usado no Android para simplificar o código async. Embora eles sejam novos no Kotlin na 1.3, o conceito de coroutines existe desde o início das linguagens de programação. A primeira linguagem a explorar usando coroutines foi Simula em 1967.

Nos últimos anos, os coroutines cresceram em popularidade e agora estão incluídos em muitas linguagens de programação populares como Javascript, C#, Python, Ruby, e Go para citar algumas. Kotlin coroutines são baseados em conceitos estabelecidos que têm sido usados para construir grandes aplicações.

No Android, coroutines são uma grande solução para dois problemas:

  1. Tarefas de longa duração são tarefas que levam muito tempo para bloquear o thread principal.
  2. Segurança principal permite assegurar que qualquer função suspensa pode ser chamada a partir da thread principal.

Vamos mergulhar em cada uma delas para ver como os coroutinos podem ajudar-nos a estruturar o código de uma forma mais limpa!

Tarefas de longa execução

Ir buscar uma página web ou interagir com uma API ambos envolvem fazer um pedido de rede. Da mesma forma, ler a partir de uma base de dados ou carregar uma imagem a partir do disco envolve ler um ficheiro. Este tipo de coisas são o que eu chamo de tarefas longas – tarefas que levam muito tempo para o seu aplicativo parar e esperar por elas!

Pode ser difícil entender quão rápido um telefone moderno executa código em comparação com uma requisição de rede. Em um Pixel 2, um único ciclo de CPU leva pouco menos de 0,0000000004 segundos, um número que é bastante difícil de entender em termos humanos. No entanto, se você pensar em um pedido de rede como um piscar de olhos, cerca de 400 milissegundos (0,4 segundos), é mais fácil entender quão rápido a CPU opera. Em um piscar de olhos, ou um pedido de rede um pouco lento, a CPU pode executar mais de um bilhão de ciclos!

No Android, cada aplicativo tem uma thread principal que é responsável por lidar com a UI (como desenhar vistas) e coordenar as interações do usuário. Se houver muito trabalho acontecendo nessa thread, o aplicativo parece pendurar ou diminuir a velocidade, levando a uma experiência de usuário indesejável. Qualquer tarefa longa deve ser feita sem bloquear a thread principal, para que seu aplicativo não exiba o que é chamado de “jank”, como animações congeladas, ou responda lentamente aos eventos de toque.

Para executar uma solicitação de rede fora da thread principal, um padrão comum é a chamada de retorno. As chamadas de retorno fornecem uma alça para uma biblioteca que pode usar para chamar de volta para o seu código em algum momento futuro. Com callbacks, buscar o arquivo developer.android.com pode se parecer com isto:

class ViewModel: ViewModel() {
fun fetchDocs() {
get("developer.android.com") { result ->
show(result)
}
}
}

Even embora get seja chamado da thread principal, ele usará outra thread para executar a requisição de rede. Então, assim que o resultado estiver disponível a partir da rede, a chamada de retorno será chamada na thread principal. Esta é uma ótima maneira de lidar com tarefas de longa execução, e bibliotecas como Retrofit podem ajudá-lo a fazer solicitações de rede sem bloquear a thread principal.

Usar coroutinas para tarefas de longa execução

Coroutinas são uma maneira de simplificar o código usado para gerenciar tarefas de longa execução como fetchDocs. Para explorar como os coroutinos tornam o código para tarefas longas em execução mais simples, vamos reescrever o exemplo acima para usar coroutinos.

// Dispatchers.Main
suspend fun fetchDocs() {
// Dispatchers.Main
val result = get("developer.android.com")
// Dispatchers.Main
show(result)
}// look at this in the next section
suspend fun get(url: String) = withContext(Dispatchers.IO){/*...*/}

Este código não bloqueia o tópico principal? Como ele retorna um resultado de get sem esperar pelo pedido e bloqueio da rede? Acontece que os coroutines fornecem uma forma de Kotlin executar este código e nunca bloquear a thread principal.

Coroutines build on regular functions, adicionando duas novas operações. Além de invocar (ou chamar) e retornar, coroutines adiciona suspender e retomar.

  • suspender – pausar a execução do coroutine atual, salvando todas as variáveis locais
  • retomar – continuar um coroutine suspenso do local onde foi pausado

Esta funcionalidade é adicionada por Kotlin pela palavra-chave suspend na função. Você só pode chamar funções suspendidas de outras funções suspendidas, ou usando um coroutine builder como launch para iniciar um novo coroutine.

Suspender e retomar o trabalho em conjunto para substituir callbacks.

No exemplo acima, get irá suspender o coroutine antes de iniciar o pedido de rede. A função get ainda será responsável por executar a requisição de rede fora da linha principal. Então, quando a requisição de rede terminar, ao invés de chamar uma chamada de retorno para notificar a linha principal, ela pode simplesmente retomar o coroutine que suspendeu.

>

Animação mostrando como Kotlin implementa a suspensão e retoma para substituir callbacks.

Vendo como fetchDocs executa, você pode ver como funciona a suspensão. Sempre que um coroutine é suspenso, a estrutura da pilha atual (o lugar que Kotlin usa para acompanhar qual função está sendo executada e suas variáveis) é copiada e salva para mais tarde. Quando ela é retomada, a moldura da pilha é copiada de volta de onde foi salva e começa a funcionar novamente. No meio da animação – quando todos os coroutinos do tópico principal estão suspensos, o tópico principal está livre para atualizar a tela e lidar com os eventos do usuário. Juntos, suspendem e retomam as chamadas de retorno. Bastante limpo!

Quando todos os coroutinos no fio principal estão suspensos, o fio principal está livre para fazer outro trabalho.

Even embora tenhamos escrito um código sequencial direto que se parece exatamente com um pedido de bloqueio da rede, os coroutinos executarão nosso código exatamente como queremos e evitarão bloquear o fio principal!

Próximo, vamos dar uma olhada em como usar coroutinos para segurança principal e explorar despachantes.

Segurança principal com coroutinos

Em Kotlin coroutines, funções suspensas bem escritas são sempre seguras para chamar a partir do fio principal. Não importa o que eles façam, eles devem sempre permitir que qualquer thread os chame.

Mas, há muitas coisas que fazemos em nossos aplicativos Android que são muito lentas para acontecer no thread principal. Pedidos de rede, analisar JSON, ler ou escrever a partir do banco de dados, ou até mesmo apenas iterar sobre grandes listas. Qualquer um destes tem o potencial de rodar devagar o suficiente para causar “jank” visível pelo usuário e deve rodar fora da thread principal.

Usar suspend não diz ao Kotlin para rodar uma função em uma thread de fundo. Vale a pena dizer claramente e com frequência que os coroutinos correrão na thread principal. Na verdade, é realmente uma boa idéia usar o Dispatchers.Main.immediate ao lançar um coroutino em resposta a um evento UI – dessa forma, se você não acabar fazendo uma tarefa longa que requer segurança principal, o resultado pode estar disponível no próximo frame para o usuário.

Coroutinas correrão no fio principal, e suspender não significa fundo.

Para fazer uma função que funcione muito devagar para o fio principal de segurança, você pode dizer a Kotlin coroutines para executar o trabalho em Default ou IO despachante. Em Kotlin, todas as coroutinas devem funcionar em um despachante – mesmo quando estão funcionando na rosca principal. Os coroutinos podem se suspender, e o despachante é o que sabe como retomá-los.

Para especificar onde os coroutinos devem rodar, Kotlin fornece três Despachadores que você pode usar para o despacho da linha.

+-----------------------------------+
| Dispatchers.Main |
+-----------------------------------+
| Main thread on Android, interact |
| with the UI and perform light |
| work |
+-----------------------------------+
| - Calling suspend functions |
| - Call UI functions |
| - Updating LiveData |
+-----------------------------------+
+-----------------------------------+
| Dispatchers.IO |
+-----------------------------------+
| Optimized for disk and network IO |
| off the main thread |
+-----------------------------------+
| - Database* |
| - Reading/writing files |
| - Networking** |
+-----------------------------------+
+-----------------------------------+
| Dispatchers.Default |
+-----------------------------------+
| Optimized for CPU intensive work |
| off the main thread |
+-----------------------------------+
| - Sorting a list |
| - Parsing JSON |
| - DiffUtils |
+-----------------------------------+

* A sala fornecerá a segurança principal automaticamente se você usar as funções de suspensão, RxJava, ou LiveData.

** As bibliotecas de rede como Retrofit e Volley gerem os seus próprios threads e não requerem segurança principal explícita no seu código quando utilizadas com Kotlin coroutines.

Para continuar com o exemplo acima, vamos utilizar os despachadores para definir a função get. Dentro do corpo de get, você chama withContext(Dispatchers.IO) para criar um bloco que será executado no despachante de IO. Qualquer código que você colocar dentro desse bloco será sempre executado no escalonador de IO. Como withContext é em si uma função suspensa, ela funcionará usando coroutinas para prover segurança principal.

// Dispatchers.Main
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
/* perform blocking network IO here */
}
// Dispatchers.Main

Com coroutinas você pode fazer o despacho de roscas com controle de granulometria fina. Porque withContext permite controlar em que thread qualquer linha de código executa sem introduzir uma chamada de retorno para retornar o resultado, você pode aplicá-lo a funções muito pequenas como leitura do seu banco de dados ou execução de uma requisição de rede. Então uma boa prática é usar withContext para ter certeza de que cada função é segura para ser chamada em qualquer Dispatcher incluindo Main – assim o chamador nunca tem que pensar em qual thread será necessária para executar a função.

Neste exemplo, fetchDocs está executando na thread principal, mas pode chamar com segurança get que executa uma requisição de rede em segundo plano. Como os coroutines suportam suspensão e retomada, o coroutine na thread principal será retomado com o resultado assim que o bloco withContext estiver completo.

As funções suspendidas bem escritas são sempre seguras para chamar a partir da thread principal (ou main-safe).

É realmente uma boa idéia fazer com que todas as funções suspendidas sejam seguras para a main-safe. Se ela fizer algo que toque o disco, a rede, ou até mesmo usar muito CPU, use withContext para torná-la segura para chamar a partir da thread principal. Este é o padrão que as bibliotecas baseadas em coroutinas, como Retrofit e Room, seguem. Se você seguir este estilo em toda a sua base de código, seu código será muito mais simples e evitará misturar as preocupações de threading com a lógica da aplicação. Quando seguidos consistentemente, os coroutines são livres para iniciar na thread principal e fazer pedidos de rede ou banco de dados com código simples, garantindo que os usuários não verão “jank”. É até possível otimizar withContext chamadas além do que é possível com callbacks em algumas situações. Se uma função fizer 10 chamadas para uma base de dados, você pode dizer ao Kotlin para trocar uma vez em uma chamada externa withContext em torno de todas as 10 chamadas. Então, mesmo que a biblioteca da base de dados chame withContext repetidamente, ela ficará no mesmo despachante e seguirá um caminho rápido. Além disso, a troca entre Dispatchers.Default e Dispatchers.IO é otimizada para evitar trocas de linha sempre que possível.

O que vem a seguir

Neste post, exploramos quais os problemas que as coroutinas são ótimas para resolver. Coroutines são um conceito muito antigo em linguagens de programação que se tornaram populares recentemente devido à sua capacidade de tornar o código que interage com a rede mais simples.

No Android, você pode usá-los para resolver dois problemas realmente comuns:

  1. Simplificando o código para tarefas longas como leitura da rede, disco, ou mesmo analisando um resultado JSON grande.
  2. Executar uma segurança principal precisa para garantir que você nunca bloqueie acidentalmente a thread principal sem tornar o código difícil de ler e escrever.

No próximo post vamos explorar como eles se encaixam no Android para manter o controle de todo o trabalho que você começou a partir de uma tela! Dê uma leitura:

Deixe uma resposta

O seu endereço de email não será publicado.