Alura > Cursos de Mobile > Cursos de Flutter > Conteúdos de Flutter > Primeiras aulas do curso Dart: entendendo assincronismo

Dart: entendendo assincronismo

Síncrono vs Assíncrono - Apresentação

Oi! Boas-vindas ao nosso curso de Dart: aplicando assincronismo.

Eu me chamo Caio Couto Moreira, mas vocês podem me chamar de Kako.

Autodescrição: Cabelo cacheado loiro, nariz comprido, olhos castanho-esverdeados e uma barba curta. Estou usando uma camisa de botões de estampada com diversos desenhos em grafite.

Este curso é essencial para você que quer se aprofundar nos conceitos de Dart, especificamente em assincronismo: objetos, métodos e funções assíncronas, que são especiais para resolvermos problemas de espera de informações.

Quando queremos esperar um dado da Internet, de uma API ou do banco de dados, precisamos começar a trabalhar de forma assíncrona. É isso que aprenderemos nesse curso.

Eu irei apresentá-lo a novos objetos que lidam com o assincronismo, por exemplo, os objetos Future e Streams. Aprenderemos como eles funcionam, como manipulá-los e qual a necessidade deles em grandes projetos, quando nos comunicarmos com objetos externos, APIs ou banco de dados de grandes empresas.

Precisamos começar a entender essa questão de esperar por um dado e das formas assíncronas de se trabalhar, mas para isso é importante que tenham feito os outros cursos de Dart. Eles serão uma base teórica para este curso.

Neste curso trabalharemos com o projeto KakoBOT, que é um robô de inteligência artificial que produzimos e que vamos manipulá-lo para entender algumas questões de como funciona o assincronismo. Nosso KakoBOT já tem a sua inteligência, então ele fala conosco, perguntando como pode nos ajudar.

Nós poderemos interagir com ele através do terminal. Por exemplo, podemos perguntar "Que horas são?" e ele irá buscar para nós o horário que nós fizemos a pergunta. Também podemos agradecê-lo e ele responderá "De nada, fique à vontade c:". E se dizemos "Adeus" ele manda uma mensagem de despedida e encerra nosso KakoBOT.

Esse projeto está pronto para usarmos e, no decorrer das aulas, entenderemos sobre assincronismo e implementaremos isso ao nosso KakoBOT, adicionando tarefas de aguardar um tempo determinando ou alguma informação. Podemos também criar um limite de uso do nosso KakoBOT, mas entenderemos tudo isso com calma durante as aulas.

Por fim, gostaria de dizer que a melhor forma de aprender neste curso é através da aprendizagem ativa, fazendo uso dos vídeos e exercícios para aprimorar nosso conhecimento. Vocês também poderão usar o Fórum e o Discord para compartilhar sua aprendizagem, suas ideias e seu conteúdo, proporcionado visão crítica, confiança, eficiência e proatividade, que o mercado de trabalho deseja.

Vamos lá?

Síncrono vs Assíncrono - Entendendo o Single Thread

Agora que sabemos qual será o projeto do curso e o que iremos aprender, vamos começar pela base. Eu quero explicar para vocês o que é o Single Thread, como são organizar as tarefas no nosso projeto. Para isso essa aula será dividida em três partes:

Dart é síncrono

Então vamos começar pelo fato do Dart ser uma linguagem síncrona, ou seja, single thread, então ele faz uma coisa por vez. Por exemplo, vamos analisar a ordem natural do nosso código.

void main() {
    String kakoBot = 'KakoBOT:/n';
    var a = true;
    String usuario = '';
    usuario = stdin.readLineSync(). toString();
    print('-- Iniciando o KakoBOT, aguarde..--');
}

É um código bem simples com void main, a String do KakoBOT, uma variável booleana true, outra String usuario vazia, que em seguida recebe o valor que o usuário escreve no teclado, através do stdin e, por fim, um print com uma mensagem que inicia o KakoBOT. Na maquinação do nosso Dart, cada linha desse código será processada por vez, então :

  1. Cria-se a string kakoBot;
  2. Cria-se a variável a
  3. Nota que ela é um booleano com valor true;
  4. Cria-se a string usuario com o valor vazio (notem que não é nulo;
  5. Associa-se o valor do usuario com a leitura de teclado (stdin) do usuário;
  6. Realiza o print no console da mensagem "Iniciando o KakoBot, aguarde".

Então é um processo por vez. De uma forma mais visual, podemos observar o GIF abaixo de como as tarefas são processadas.

Observação: A animação demora um pouco para ser iniciada.

Gif representando o processo das tarefas em sincronismo. Em um retângulo branco, uma seta cinza o corta horizontalmente pela metade, apontando da esquerda para direita e parando na metade vertical da tela. Sobre a haste da seta há três figuras geométricas. Da esquerda para direita são um círculo azul, um quadrado laranja e um pentágono roxo, sendo que o pentágono é a figura mais próxima da ponta da seta e o círculo a mais distante. Depois da ponta da seta aparece uma grande engrenagem cinza com duas setas formando um círculo, onde a ponta de cada seta aponta para o final da outra. Elas indicam que a engrenagem gira da esquerda para direita. Quando a engrenagem começa a girar, o pentágono roxo avança para engrenagem, encaixa em um dos espaços e gira junto com ela até cair no canto inferior direito do retângulo branco do fundo. Quando ele cai, o quadrado, que é a figura seguinte, faz o mesmo movimento na engrenagem, caindo no mesmo lugar e se sobrepondo ao pentágono. Por fim, o círculo faz o mesmo processo, se sobrepondo ao quadrado.

Então temos três tarefas: uma roxa, uma laranja e uma azul. Essas tarefas estão em um tipo de esteira e estão em uma ordem. A tarefa roxa está na frente, a laranja no meio e a azul no final.

O Dart fará um loop, representado pela engrenagem girando, que vai processar a tarefa roxa, depois a tarefa laranja e depois a azul, uma coisa de cada vez. Portanto imaginem esse conceito de esteira e engrenagem quando falamos sobre single threads.

Nós não conseguimos fazer duas tarefas ao mesmo tempo, precisamos esperar uma tarefa terminar seu percurso na engrenagem para começarmos outra tarefa, até terminar toas as tarefas necessárias no nosso projeto. É isso que quero dizer quando explico que o Dart é síncrono e uma linguagem single thread.

Isolates e loops events

Esse tema é mais avançado, e acho legal vocês saberem. Além disso, para quem já está inserido no Flutter e na linguagem de Dart, terá uma experiência nova em relação à aspectos mais avançados.

Quando usamos Dart, como qualquer linguagem de programação, precisamos usar a memória do nosso computador, que é uma parte física que fica no processador do computador. Nós precisamos alocar parte dessa memória para o uso do Dart.

A seguir temos duas imagens com duas representações de memória que chamaremos de isolates:

Representação de dois isolates, lado a lado, no formato de dois retângulos verticais de fundo branco. Na metade superior de cada retângulo tem uma tabela de quatro linhas e três colunas, já na metade inferior há duas setas formando um círculo onde a ponta de cada seta aponta para o final da outra. Na tabela do retângulo da esquerda, a primeira linha as cédulas da primeira e da segunda coluna estão preenchidas de verde. Na segunda linha as cédulas da segunda e da terceira coluna também estão preenchidas de verde. Na terceira linha todas as cédulas estão preenchidas de verde, mas a cédula da segunda coluna está em um tom mais claro de verde. Na quarta linha todas as cédulas estão preenchidas de verde. Já na tabela do retângulo da direita, todas as cédulas estão preenchidas de verde.

Cada um desses blocos de memória, ou seja, cada retângulo verde, é responsável por finalizar uma tarefa. Portanto, se o Dart é assíncrono, ele usará só um bloco de memória.

Entretanto, na imagem temos dois modelos de isolates, e eu decidi colocar dois para vocês verem a diferença entre um isolate em uso (o da esquerda) e um completamente parado (o da direita). Na figura da direita conseguimos notar que tem alguns espaços em branco e na terceira linha e segunda coluna tem uma cédula com um tom bem claro de verde.

O que acontece é que, quando estamos concluindo uma tarefa, partes do nosso bloco de memória estão sendo usados, e não podemos usar dois blocos físico de memória ao mesmo tempo. Isso significa que é fisicamente impossível fazer duas tarefas simultâneas. Então na direita temos o isolate parado, ou seja, sem estar em uso.

Recapitulando:

Portanto isolates são espaços de memória dedicada para finalizar as tarefas do nosso projeto. Contudo, pensem no que acontece se essa tarefa demorar, às vezes nem por nossa culpa, mas porque ela precisa esperar por um valor.

Como é uma fila, existem vários problemas que podem acontecer se a tarefa demorar. O primeiro problema que pode acontecer é o atraso de tarefas. Então, voltando para o modelo da engrenagem, se a tarefa laranja trava a fila, ou seja, demora para ser concluída, a tarefa azul ficará esperando até que a conclusão da tarefa anterior ocorra.

Isso pode gerar, em muitos projetos, erro em tempo de execução, que é o famoso aplicativo travado. Para quem está trabalhando com Flutter já deve até ter visto alguns desses erros. E esse é um dos piores erros que podemos ter no mundo da programação, ou seja, não queremos que aconteça.

Essa demora na execução na tarefa também pode gerar dependências quebradas. Já aprendemos muito sobre isso, como criar classes, elas dependerem de outras classes. Isso aconteceu em um dos nossos primeiros exemplos, onde uma classe Fruta era "pai" de uma classe Citrica.

Então se a classe "Fruta" demorar para ser criada, a classe "Cítrica" também terá problemas. Portanto as dependências quebras e podem gerar problemas também.

Assincronismo Dart

Com isso notamos que tarefas que demoram muito atrapalham todo fluxo, então vou explicar sobre o assincronismo Dart, que foi a forma que encontramos para solucionar o problema de tarefas que demoram. Portanto, vamos voltar para o problema que é: temos duas tarefas para serem concluídas e, em uma delas, precisamos esperar por um valor.

Nós não precisamos processar essa tarefa, ou seja, fazer a parte trabalhosa, apenas precisamos esperar um valor que pode estar vindo de algum lugar, como da Internet. Nesse caso, qual seria a melhor alternativa para solucionar esse problema. Poderíamos:

Criar dois espaços de memória seria criar um segundo isolate, o que pode resolver nosso problema, mas deixa nosso projeto muito mais pesado, exigindo mais recursos, e não queremos isso. Porque o Dart é naturalmente single thread, e criando um novo espaço de memória, transformamos ele em multi-thread.

Essa solução pode ser legal, mas nem todos nosso problemas precisam ser solucionados criando novos espaços de memória. Sendo assim, essa não é a melhor alternativa.

No caso da criação de dois loops de memória é o equivalente a criar duas "engrenagens" para executar as tarefas. Entretanto, se um loop de memória está demorando para fazer uma tarefa e outro loop coloca mais uma tarefa em cima dela, teremos um problema.

Nós não temos espaço na memória para duas tarefas serem feitas ao mesmo tempo, portanto, essa não é uma alternativa plausível. Dessa forma a resposta correta é nenhuma das alternativas.

A resposta correta é utilizar o assincronismo, que é como um call-back, onde esperamos o momento certo para finalizar uma tarefa. Então se é uma tarefa na qual precisamos esperar por um valor, o ideal é deixar essa tarefa mais ao final na nossa fila de execução de tarefas.

Na próxima aula vamos conversar mais sobre assincronismo para entendermos a grande importância para tarefas que demoram a ser concluídas e porque essa é uma boa alternativa para o nosso problema.

Síncrono vs Assíncrono - Definindo assincronismo

Conversamos um pouco sobre single thread e assincronismo, sem nos aprofundarmos muito. Isso porque aprenderemos mais sobre assincronismo nessa aula.

Eu vou explicar para que o assincronismo serve, quando usamos e quando é melhor usá-lo. Para isso, nessa aula também teremos três tópicos:

Assincronismo na computação

O assincronismo realmente é necessário? Precisamos mesmo saber lidar com tarefas que demoram?

Em muitos projetos nós precisamos conversar com o exterior, por exemplo, um banco de dados, porque nossos projetos têm variáveis e informações. Grandes projetos como bancos e aplicativos famosos têm um servidor dedicado com informações que estão em outro lugar, ou seja, não estão no seu celular, e sim seguras em um servidor.

Podemos manipular esses dados da seguinte forma:

Então existem vários formatos e ações que podemos fazer com dados externos, mas o grande problema de usar informações de fora do nosso projeto é a demora que as coisas levam para acontecer. Por exemplo, já acessaram um site que demorou muito para recarregar, por mais que você tentasse recarregá-lo?

Isso não é culpa do site, e sim do servidor que hospeda as informações externas, então existe um delay e precisamos esperar por esses dados. Apesar da informação ser rápida, existe um tempo de espera mínimo.

Outro exemplo é o acesso à APIs. Para quem não sabe, e essa informação não é fundamental para este curso de assincronismo no Dart, APIs são Application Programming Interface (Interface de Programação de Aplicação). Elas possuem normas e protocolos para que possamos ter acesso à rede, e quando falamos em rede, nos referimos à Internet.

Por exemplo, queremos uma lista dos 100 melhores filmes de ação vinda da Internet ou uma foto do Google. Para termos acesso a essas informações temos também um tempo de espera, que precisa ser contabilizado.

Vamos imaginar que usamos o código de uma imagem da Internet que demora para ser carregada. Enquanto esperamos por ela, o projeto fica parado esperando que ela seja processada, e ele não conseguirá fazer nada, por mais que tentemos inicializá-lo.

A melhor forma de lidar com isso é usando o assincronismo, esperando essa imagem ser carregada. Isso porque essa informação não depende de nós, e sim da conexão com a Internet, que vai determinar o tempo de espera.

Enquanto isso, podemos fazer outras tarefas no nosso projeto. Essa é a grande ideia por trás do assincronismo: deixar tarefas que não dependem de nós carregando no final da fila de execução, enquanto fazemos outras atividades.

Outro exemplo que quero mostrar para vocês é o do Flutter, um framework que usa o Dart, como vocês podem observar nas imagens abaixo.

Tela de um aplicativo em Flutter. A App Bar é azul claro e está escrito "Tarefas". Abaixo dela há uma lista com vários cartões retangulares horizontais. Esses cartões são formados por um retângulo branco maior e, abaixo dele, um retângulo azul menor. No retângulo branco há uma imagem à esquerda, ao lado direito dela há o título da atividade com 5 estrelas embaixo, preenchidas de acordo com o nível de dificuldade da tarefa, e no lado direito interno do retângulo branco tem um botão quadrado com uma seta para cima e a palavra "UP" embaixo da seta. No retângulo azul há uma linha de progresso à esquerda. O começo da linha está em branco opaco e o resto em um tom mais transparente de branco. No lado direito do retângulo azul tem escrito "Nível:1" em todas as atividades. Como exemplo, o primeiro cartão da lista tem a imagem do Dash, mascote do Flutter. O título da tarefa é "Estudar Flutter" e três estrelas estão preenchidas em azul claro, deixando duas em um tom mais transparente de azul. Além disso, no canto inferior direito da tela há um botão flutuante circular azul claro com o sinal de adição dentro dele.

Outra tela do mesmo aplicativo de Flutter. Na app bar está escrito "Nova Tarefa" Abaixo dela há um retângulo cinza claro contornado de preto. Dentro dele há três campos de texto em forma de lista, sendo eles, de cima para baixo, "Nome", "Dificuldade" e "Imagem". Abaixo do campo "Imagem" há um pequeno retângulo vertical azul claro centralizado horizontalmente na tela. Dentro dele há um ícone de uma câmera cortada pelo sinal de proibido. Abaixo desse retângulo há um botão retangular horizontal escrito "Adicionar!".

Importante: Relembrando que não é necessário saber Flutter para fazer o curso de Dart: aplicando assincronismo.

Esse exemplo é interessante porque existem botões nos aplicativos de Flutter. Esses botões ficam parados, esperando serem pressionados. No caso desse aplicativo, quando clicamos no botão de "+", no canto inferior direito, ele muda de tela.

Contudo, até pressionarmos esse botão ele não muda de tela. Esse é o exemplo de uma tarefa que fica esperando por uma ação, e enquanto esperamos essa ação, existem outras tarefas no app. Podemos querer usar outros botões do aplicativo ou arrastar a tela para olhar outras informações.

Sendo assim, é necessário que possamos fazer ações enquanto há tarefas em espera, ou o aplicativo permanecerá travado enquanto tiver um botão esperando uma ação. Desse forma, é imprescindível termos a habilidade de trabalhar com tarefas que precisam de tempo de espera e outras que podem ser executadas a qualquer momento.

Exemplos famosos

Um primeiro e grande exemplo é o WhatsApp. Ele é um app de comunicação que troca mensagens em "tempo real". Eu digo "tempo real", entre aspas, porque demora um pouco.

Por exemplo, o WhatsApp pode parecer uma conversa entre duas pessoas, mas é uma conversa a três, a primeira pessoa, que é a emissora da mensagem, manda mensagem para um servidor, que avisa para segunda pessoa, que é a receptora, que a primeira está digitando. Por fim, o servidor manda para pessoa receptora a mensagem.

Então tem o caminho "Emissora > Servidor > Receptora" pela qual a mensagem passa antes de chegar. Se enquanto a mensagem não chega o WhatsApp travasse, seria complicado e frustrante, mas ele nos permite acessar outras conversas e mandar outras mensagens. Enquanto isso, ele espera a mensagem ser recebida ou enviada.

Então é importante entendermos o assincronismo nesse caso. Lembrando que quando falamos que é necessário esperar por algo, pensamos no conceito de delay, ou seja, um tempo de espera/atraso de um processo ou tarefa. É importante saberem esse conceito, porque usaremos muito ele no futuro para simular nosso assincronismo.

Outro exemplo famoso é o Instagram, que é uma rede social onde podemos compartilhar fotos e vídeos. Ele tem um sistema de curtida e de chat, ou seja, vários features, cada um com seu tempo de processo que precisa ser esperado.

Na prática, imaginem que estão com um belíssimo doce que vocês querem fotografar para postar nas redes sociais. Quando você fotografa e clica para compartilhar no Instagram, ele tem um tempo de espera para transformar a foto do seu celular em uma foto do Instagram e depois enviar essa foto para o servidor do Instagram.

Enquanto ele está enviando a foto para o servidor do Instagram e postando nas redes sociais, seria estranho se o Instagram travasse. Porém ele permite que enquanto você envia a postagem para o servidor, você pode usar sua rede social, vendo outras fotos, usando o chat, curtindo postagens. Tudo isso porque ele está fazendo duas coisas ao mesmo tempo: esperando o resultado enquanto te permite fazer outras tarefas.

Acho importante informar que isso não se aplica só ao Dart, tanto que, até onde eu sei, o WhatsApp e o Instagram não se aplicam apenas em Dart. Entretanto, ambos usam o assincronismo, porque vários projetos o usam.

Assincronismo no Dart

Agora vou explicar rapidamente como usar o assincronismo no Dart, porque teremos várias aulas específicas de como aplicá-lo. Começarei, então, explicando sobre o objeto Future, que represente uma variável que só estará disponível futuramente.

Lembrete: Future é objeto que representa uma variável que só estará disponível no futuro.

Com o objeto Future, podemos implementar o assincronismo e deixar uma espera na nossa linha de código. Por exemplo, no código abaixo temos um objeto Future que espera três segundos e, após esse tempo, ele faz um print e retorna o valor 12.

Future<int> myFutureFunc() async {
    await Future.delayed(Duration(second: 3));
    print('I have a function in the Future!');
    return 12;

I have a function in the Future!

12

Se conseguimos esperar para ações acontecerem, então conseguimos aplicar o assincronismo, basta entendermos como ele funciona.

Outro objeto que temos no Dart é o Stream, uma sequência e informações ou eventos que podem aparecer a qualquer momento, que sequer sabemos quando. Então é outro objeto que nos permite fazer tarefas enquanto informações estão aparecendo no nosso caminho.

Lembrete: Stream é um objeto que recebe uma sequência de dados/eventos assíncronos.

Stream<int> timedCounter(int interval, [int? maxCount]) async* {
    int i = 1;
    while(i != maxCount) {
        await Future.delayed(Duration(second: interval));
        yield i++;
}

Caso ainda não tenham entendido o Future e o Stream, não se preocupem. Por enquanto, entendam apenas porque precisamos do assincronismo, porque ele é importante para nós e que existem formas de aplicá-lo no Dart.

Mas antes de passarmos diretamente para os objetos Future e Stream no Dart, no próximo vídeo quero mostrar para vocês onde aplicaremos nosso conhecimento e como nosso projeto irá funcionar.

Sobre o curso Dart: entendendo assincronismo

O curso Dart: entendendo assincronismo possui 149 minutos de vídeos, em um total de 38 atividades. Gostou? Conheça nossos outros cursos de Flutter em Mobile, ou leia nossos artigos de Mobile.

Matricule-se e comece a estudar com a gente hoje! Conheça outros tópicos abordados durante o curso:

Aprenda Flutter acessando integralmente esse e outros cursos, comece hoje!

Conheça os Planos para Empresas