Neste artigo, falarei sobre como as corotinas funcionam e como criá-las. Considere o aplicativo em execução sequencial e paralela. Vamos falar sobre tratamento de erros, depuração e maneiras de testar a corotina. No final, resumirei e falarei sobre as impressões que permaneceram após a aplicação dessa abordagem.
O artigo foi preparado com base nos materiais do meu relatório no
MBLT DEV 2018 , no final do post - um link para o vídeo.
Estilo consistente
Fig. 2.1Qual foi o objetivo dos desenvolvedores Corutin? Eles queriam que a programação assíncrona fosse o mais simples possível. Não há nada mais fácil do que executar o código “linha por linha” usando as construções sintáticas da linguagem: try-catch-finalmente, loops, instruções condicionais e assim por diante.
Vamos considerar duas funções. Cada um é executado em seu próprio thread (Fig. 2.1). O primeiro é executado no encadeamento
B e retorna alguns dados do resultadoB, então precisamos passar esse resultado para a segunda função, que aceita o
dataB como argumento e já está sendo executado no encadeamento
A. Com a corotina, podemos escrever nosso código como mostrado na fig. 2.1 Considere como conseguir isso.
As funções
longOpOnB, longOpOnA são as chamadas funções de
suspensão , antes das quais o encadeamento é liberado e, após o término, ele fica ocupado novamente.
Para que essas duas funções sejam realmente executadas em um encadeamento diferente em relação ao chamado, mantendo um estilo "consistente" de escrever código, devemos mergulhá-las no contexto da corotina.
Isso é feito através da criação de corotinas usando o chamado Coroutine Builder. Na figura, este é o
lançamento , mas existem outros, por exemplo,
assíncrono ,
runBlocking . Falarei sobre eles mais tarde.
O último argumento é um bloco de código executado no contexto da corotina: chamar funções de suspensão, o que significa que todo o comportamento acima é possível apenas no contexto da corotina ou em outra função de suspensão.
Existem outros parâmetros no método Coroutine Builder, por exemplo, o tipo de inicialização, o encadeamento no qual o bloco será executado e outros.
Gerenciamento do ciclo de vida
O Coroutine Builder nos fornece o valor de retorno como valor de retorno - uma subclasse da classe
Job (Fig.2.2). Com ele, podemos gerenciar o ciclo de vida de corutin.
Comece com o método
start () , cancele com o método
cancel () , aguarde a conclusão da tarefa usando o método
join ( ), inscreva-se no evento de conclusão da tarefa e muito mais.
Fig. 2.2Mudança de fluxo
Você pode alterar o fluxo de execução da corotina alterando o elemento de contexto da corotina responsável pelo planejamento. (Fig. 2.3)
Por exemplo, a corutin 1 será executada em um encadeamento da
interface do usuário , enquanto a corutin 2 em um encadeamento retirado do pool
Dispatchers.IO .
Fig.2.3A biblioteca de corotina também fornece uma função de suspensão
comContext (CoroutineContext) , com a qual você pode alternar entre os segmentos no contexto de uma corotina. Portanto, saltar entre os threads pode ser bastante simples:
Fig. 2.4Iniciamos a nossa rotina na rosca da interface do usuário 1 → mostramos o indicador de carga → alternamos para a rosca de trabalho 2, liberando a principal → realizamos uma operação longa que não pode ser executada na rosca da interface do usuário → retornamos o resultado para a rosca da interface do usuário 3 → e já trabalhamos lá com ele, renderizando os dados recebidos e ocultando o indicador de carregamento.
Parece bastante confortável até agora, siga em frente.
Suspender Função
Considere o trabalho de corutin no exemplo do caso mais comum - trabalhando com solicitações de rede usando a biblioteca Retrofit 2.
A primeira coisa que precisamos fazer é converter a
chamada de retorno de chamada em uma função de
suspensão para tirar proveito do recurso de rotina:
Fig. 2.5Para controlar o estado da corotina, a biblioteca fornece funções no formato
suspendXXXXCoroutine , que fornecem um argumento que implementa a interface
Continuation , usando os métodos
resumeWithException e
resume dos quais podemos retomar a corotina em caso de erro e sucesso, respectivamente.
A seguir, descobriremos o que acontece quando o método resumeWithException for chamado e, primeiro, teremos o cuidado de precisar cancelar de alguma forma a chamada de solicitação de rede.
Suspender função. Cancelamento de chamadas
Para cancelar a chamada e outras ações relacionadas à liberação de recursos não utilizados, ao implementar a função de suspensão, você pode usar o método
suspendCancellableCoroutine que
sai da caixa (Fig. 2.6). Aqui, o argumento de bloco já implementa a interface
CancellableContinuation , um dos métodos adicionais
chamado invokeOnCancellation , que permite que você se inscreva em um erro ou em um evento bem-sucedido de cancelamento de rotina. Portanto, aqui também é necessário cancelar a chamada do método.
Fig. 2.6Exibir alterações na interface do usuário
Agora que a função de suspensão foi preparada para solicitações de rede, você pode usar sua chamada no fluxo da interface do usuário da Coroutine como seqüencial, enquanto durante a execução da solicitação o fluxo estará livre e o fluxo de retroajuste será usado para a solicitação.
Assim, implementamos o comportamento assíncrono em relação ao fluxo da interface do usuário, mas o escrevemos em um estilo consistente (Figura 2.6).
Se, depois de receber a resposta, você precisar fazer o trabalho duro, por exemplo, gravando os dados recebidos no banco de dados, essa função, como já foi mostrada, poderá ser facilmente executada usando
withContext no pool de fluxos de back-stream e continue a execução na interface do usuário sem uma única linha de código.
Fig. 2.7Infelizmente, isso não é tudo o que precisamos para o desenvolvimento de aplicativos. Considere o tratamento de erros.
Tratamento de erros: try-catch-finalmente. Cancelar Coroutine: CancellationException
Uma exceção que não foi capturada dentro da corotina é considerada não tratada e pode levar à falha do aplicativo. Além de situações normais, uma exceção é lançada retomando a corotina usando o método
resumeWithException na linha correspondente da chamada à função de suspensão. Nesse caso, a exceção passada como argumento é lançada inalterada. (Fig. 2.8)
Fig. 2.8Para manipulação de exceções, a construção padrão de tentativa de captura finalmente de linguagem está disponível. Agora, o código que pode exibir o erro na interface do usuário assume o seguinte formato:
Fig. 2.9No caso de cancelar a corotina, que pode ser obtida chamando o método Job # cancel,
é lançada uma
CancellationException . Essa exceção é tratada por padrão e não causa falhas ou outras consequências negativas.
No entanto, ao usar a construção
try / catch , ela será capturada no
bloco catch e você precisará considerar isso nos casos se desejar lidar apenas com situações realmente "errôneas". Por exemplo, o tratamento de erros na interface do usuário quando é possível "cancelar" solicitações ou o log de erros é fornecido. No primeiro caso, o erro será exibido para o usuário, embora ele realmente não exista, e no segundo, uma exceção inútil será registrada e desorganizará os relatórios.
Para ignorar a situação de cancelamento de corotinas, você precisa modificar levemente o código:
Fig. 2.10Registro de erro
Considere o rastreamento de pilha de exceção de exceção.
Se você lançar uma exceção diretamente no bloco de código da corotina (Fig. 2.11), o rastreamento da pilha parecer limpo, com apenas algumas chamadas da corotina, indica corretamente a linha e as informações sobre a exceção. Nesse caso, é possível entender facilmente a partir do rastreamento da pilha onde exatamente, em qual classe e em qual função a exceção foi lançada.
Fig. 2.11No entanto, as exceções que são passadas para o método
resumeWithException de funções de
suspensão , como regra, não contêm informações sobre a corotina na qual ocorreu. Por exemplo (Fig. 2.12), se você retomar a corotina da função de suspensão implementada anteriormente com a mesma exceção do exemplo anterior, o rastreamento da pilha não fornecerá informações sobre onde procurar especificamente o erro.
Fig. 2.12Para entender qual coroutine recomeçou com uma exceção, você pode usar o
elemento de contexto
CoroutineName . (Fig. 2.13)
O elemento
CoroutineName é usado para depuração, passando o nome da corotina para ele, você pode extraí-lo nas funções de suspensão e, por exemplo, complementar a mensagem de exceção. Ou seja, pelo menos ficará claro onde procurar um erro.
Essa abordagem funcionará apenas se a função de suspensão for excluída disso:
Fig. 2,13Erro no registro. ExceptionHandler
Para alterar o log de exceções para uma corotina específica, você pode definir seu próprio ExceptionHandler, que é um dos elementos do contexto da corotina. (Fig. 2.14)
O manipulador deve implementar a interface
CoroutineExceptionHandler . Usando o operador + substituído para o contexto da rotina, você pode substituir o manipulador de exceção padrão pelo seu. A exceção não tratada se enquadra no método
handleException , onde você pode fazer o que for necessário. Por exemplo, ignore completamente. Isso acontecerá se você deixar o manipulador vazio ou adicionar suas próprias informações:
Fig. 2,14Vamos ver como pode ser o registro da nossa exceção:
- Você precisa se lembrar da CancellationException , que queremos ignorar.
- Adicione seus próprios logs.
- Lembre-se do comportamento padrão, que inclui registrar e encerrar o aplicativo, caso contrário, a exceção simplesmente "desaparecerá" e não ficará claro o que aconteceu.
Agora, no caso de lançar uma exceção, uma lista de rastreamento de pilha será enviada ao logcat com as informações adicionadas:
Fig. 2,15Execução paralela. assíncrono
Considere a operação paralela das funções de suspensão.
O Async é mais adequado para organizar resultados paralelos de várias funções. Assíncrono, como o
lançamento - Coroutine Builder. Sua conveniência é que, usando o método waitit
() , ele retorne dados se for bem-sucedido ou gere uma exceção que ocorreu durante a execução da corotina. O método de espera aguardará a conclusão da rotina, se ainda não estiver concluída, caso contrário, retornará imediatamente o resultado do trabalho. Observe que wait é uma função de suspensão e, portanto, não pode ser executada fora do contexto de uma corotina ou outra função de suspensão.
Usando async, obter dados de duas funções em paralelo será algo como isto:
Fig. 2,16Imagine que somos confrontados com a tarefa de obter dados de duas funções em paralelo. Então, você precisa combiná-los e exibir. Em caso de erro, é necessário desenhar a interface do usuário, cancelando todas as solicitações atuais. Tal caso é freqüentemente encontrado na prática.
Nesse caso, o erro deve ser tratado da seguinte maneira:
- Traga o tratamento de erros dentro de cada um dos assíncronos-corutinos.
- Em caso de erro, cancele todas as corotinas. Felizmente, para isso, é possível especificar um trabalho pai, no cancelamento do qual todos os seus filhos são cancelados.
- Criamos uma implementação adicional para entender se todos os dados foram carregados com êxito. Por exemplo, assumimos que, se aguardar retornado nulo, ocorreu um erro ao receber dados.
Com tudo isso em mente, a implementação da rotina dos pais está se tornando um pouco mais complicada. A implementação de async-corutin também é complicada:
Fig. 2,17Essa abordagem não é a única possível. Por exemplo, você pode implementar a execução paralela com o tratamento de erros usando
ExceptionHandler ou
SupervisorJob .
Corotinas aninhadas
Vejamos o trabalho da corotina aninhada.
Por padrão, a rotina aninhada é criada usando um escopo externo e herda seu contexto. Como resultado, a corotina aninhada se torna filha e o pai externo.
Se cancelarmos a corotina externa, as corotinas aninhadas criadas dessa maneira, usadas no exemplo anterior, também serão canceladas. Também será útil ao sair da tela quando você precisar cancelar as solicitações atuais. Além disso, o pai corutin sempre aguardará a conclusão da filha.
Você pode criar uma rotina independente da externa usando um escopo global. Nesse caso, quando a corotina externa é cancelada, a aninhada continuará funcionando como se nada tivesse acontecido:
Fig. 2,18
Você pode criar um filho da corotina aninhada global substituindo o elemento de contexto pela chave
Job pelo trabalho pai ou pode usar totalmente o contexto da corotina pai. Mas, neste caso, vale lembrar que todos os elementos da rotina principal são assumidos: o pool de threads, o manipulador de exceções e assim por diante:
Fig. 2,19Agora está claro que, se você usar a rotina externa, precisará fornecer a eles a capacidade de instalar uma instância do trabalho ou o contexto do pai. E os desenvolvedores de bibliotecas precisam considerar a possibilidade de instalá-lo quando criança, o que causa transtornos.
Pontos de interrupção
As corotinas afetam a exibição dos valores do objeto no modo de depuração. Se você colocar um ponto de interrupção na próxima corotina na função
logData , quando for acionado, veremos que tudo está bem aqui e os valores são exibidos corretamente:
Fig. 2,20Agora obtenha
dataA usando a rotina aninhada, deixando um ponto de interrupção no
logData :
Fig. 2,21Tentar expandir o bloco this para tentar encontrar os valores desejados falha. Assim, a depuração na presença de funções de suspensão se torna difícil.
Teste de unidade
O teste de unidade é bastante direto. Você pode usar o
runBlocking do Coroutine Builder
para isso .
O runBlocking bloqueia um encadeamento até que todas as suas corotinas aninhadas sejam concluídas, exatamente o que você precisa para testar.
Por exemplo, se for sabido que em algum lugar dentro do método coroutine é usado para implementá-lo, para testar o método, você só precisa envolvê-lo no
runBlocking .
O runBlocking pode ser usado para testar uma função de suspensão:
Fig. 2,22Exemplos
Finalmente, gostaria de mostrar alguns exemplos do uso de corutin.
Imagine que precisamos executar três consultas A, B e C em paralelo, mostrar sua conclusão e refletir o momento de conclusão das solicitações A e B.
Para fazer isso, você pode simplesmente agrupar as corotinas de consulta A e B em uma comum e trabalhar com ela como um único todo:
Fig. 2,23O exemplo a seguir demonstra como usar o loop for regular para executar consultas periódicas com um intervalo de 5 segundos:
Fig. 2,24Conclusões
Das desvantagens, noto que as corotinas são uma ferramenta relativamente jovem; portanto, se você quiser usá-las no produto, faça isso com cautela. Existem dificuldades na depuração, um pequeno padrão na implementação de coisas óbvias.
Em geral, as corotinas são bastante fáceis de usar, especialmente para implementar tarefas assíncronas não complicadas. Em particular, devido ao fato de que construções de linguagem padrão podem ser usadas. As corotinas são facilmente passíveis de teste de unidade e tudo isso sai da caixa da mesma empresa que desenvolve o idioma.
Denunciar vídeo
Acabou muitas cartas. Para quem gosta de ouvir mais - vídeo do meu relatório no
MBLT DEV 2018 :
Materiais úteis sobre o tema: