Sejam muito bem-vinda e bem-vindo ao curso de Dart: lidando com Exceptions e com Null Safety. O meu nome é Ricarth Lima e vou ser seu instrutor nesse curso. Por uma questão de acessibilidade vou me autodescrever agora. Sou um homem, tenho cor de pele parda, tenho cabelo bem cacheado próximo do crespo, tenho sobrancelhas grossas, uso óculos com armação preta, tenho um grande nariz, uso barba de um pouco grande e na maioria desse curso usarei uma camisa cinza.
Nesse curso começaremos conversando a respeito do projeto que servirá de base para todo o curso. Em cima desse projeto, conversaremos sobre a diferença entre erros e exceções. Logo na sequência, vamos aprender o que é e como se relaciona com o tema a pilha de execução. É bem importante.
Logo depois, descobriremos o que é o depurador e o modo de depuração que temos nas nossas linguagens de programação, em especial no Dart. Na sequência, aprenderemos como lidar com as exceções e os erros que podem ser gerados pelo nosso programa, usando a estrutura Try On Catch Finally. Logo depois, veremos a importância de criarmos nossas próprias exceções personalizadas para que elas consigam atender aos problemas excepcionais do modelo de negócios.
Então vamos aprender a criá-las, descrever suas propriedades e métodos e a usá-las. Por fim, vamos falar de um assunto que é extremamente importante para você que está aprendendo Dart e para você que veio do Flutter para se aprofundar no Dart, que é o Null Safety ou a Segurança de Nulos, que é um assunto que importa bastante para que consigamos criar códigos que são seguros em tempo de execução.
Ao final, teremos o seguinte projeto. Vamos criar um projeto de um banco que vai ser bem simples. Ele só comporta algumas contas e faz transferências entre contas, mas vamos visualizar que nem sempre essas transferências vão dar certo. Para isso, vamos usar as nossas exceções. Então no arquivo main teremos o nosso banco rodando e nos demais arquivos teremos todo o aparato para tornar a experiência do usuário cada vez melhor usando as ferramentas citadas.
É bem interessante que para você continuar nesse curso, você tenha uma base de Orientação a objetos, de Sintaxe e de Coleções. Então, para isso, recomendo que você tenha assistido o nosso segundo curso que é Dart: Orientação a Objetos e o terceiro curso que é Dart: Coleções, Sintaxe e Encapsulamento. É bem importante que você tenha essa base. Dito isso, te vejo no curso. Até lá.
Agora que conhecemos o projeto final do curso e o propósito desse curso, chegou a hora de conhecermos o projeto que vai servir de base durante todo o nosso estudo. O código é bem simples e ele vai estar disponível na atividade na sequência desse vídeo, mas vamos analisá-lo detalhadamente.
Temos duas classes que vamos usar, que estão separadas em arquivos, e temos o nosso arquivo principal. Então se acessarmos "bin > models", teremos o arquivo “account” dentro dele. Vou clicar duas vezes no arquivo “account”. Esse é um momento bem propício de falar que esse curso vai ter todos os termos em inglês.
O principal motivador disso é que se defende que você ter duas linguagens, no caso, a língua portuguesa para comunicar variáveis, nomes de funções e classes e a própria Língua Inglesa usando a sintaxe da linguagem, deixa mais difícil de compreender o código. Então, como esse já é um curso avançado, usaremos termos em inglês. Só usaremos termos em português nas saídas para o usuário.
Então account, que significa conta, tem três propriedades: string name, que seria o nome de quem tem essa conta, o double balance, que seria o saldo disponível nessa conta, e um booleano is authenticated, que basicamente diz se está autenticado ou não, se a pessoa logou nessa conta. Claro, tudo para fins de exemplo. Então teremos o construtor de account, que vai alimentar esses parâmetros, todos obrigatórios, e ele vai ter apenas um método que seria o "Editar Saldo", editBalance()
, que recebe um valor e soma a esse saldo esse valor.
Entendido o account, acessaremos “controllers > bank_controller”. O “bank_controller” será um controlador do banco que estamos simulando. Não vamos fazer as operações direto na main. É mais organizado que tenhamos classes que modularizem essas operações, e para isso temos a classe “bank_controller”.
A “bank_controller” controla qualquer instância dela, e vamos usar uma dela na main. Vai ter um database que será uma propriedade privada, que é basicamente um map de string account, ou seja, que ela tem um string, que é um ID, e um account, que é uma instância da classe que acabamos de apresentar.
Então temos um método que adiciona uma conta nesse banco requirindo um string como ID e uma própria account, que já foi pré-definida. Temos um método de fazer uma transferência, que é basicamente de transferir um saldo de uma conta para outra. Então é bem interessante de analisarmos esse método porque ele é o que vai servir de base, principalmente para as nossas operações nesse curso.
Ele terá um ID de quem está enviando, esse idSender
, ele vai ter um ID de quem está recebendo, então um idReceiver
, e ter um amount
que será a quantidade transferida. E vamos tentar fazer justamente isso, pegar a conta de quem está enviando, tirar de quem está enviando e adicionar em quem está recebendo.
Então podemos notar que, normalmente, faríamos só o caminho feliz. Vamos falar disso mais adiante, mas entendemos que, comumente, não testaríamos condições que não funcionaria. Nesse exemplo, caso passemos uma quantidade, verificamos se o remetente realmente tem uma quantidade na conta que ele quer passar. Se ele não tiver, em todas as outras condições excepcionais, vamos retornar um false.
Se tudo estiver correto, faremos sim o caminho feliz, que seria, quem está enviando perde essa quantidade e quem está recebendo ganha essa quantidade, e no final retornamos um true. Outros testes que fazemos além desses é se o ID é válido, ou seja, se existe alguém no banco com esse ID e se o ID do destinatário é válido. Então precisamos testar tanto do remetente quanto do destinatário, se são válidos.
Claro, pegamos a instância deles que está no nosso banco. Por fim, testamos se quem está enviando está autenticado, e finalmente testamos a quantidade, como explicamos para poder fazer a operação. Além disso, temos só mais um método que faz uma verificação rápida, se o banco de dados contém o ID que usamos para fazer essa verificação.
Além do controlador do banco, temos a main, onde tudo acontece. Então começamos criando um banco, digamos assim, ou seja, construindo uma instância de “bank_controller”, depois adicionamos algumas contas, no caso serão duas. Primeiro adicionamos a conta Ricarth, que vai ter como ID Ricarth, tem o account como segundo parâmetro para adicionar essa conta, e terá um nome, um saldo e se está autenticado ou não.
A mesma coisa faremos com o Kako, e podemos notar que o ID pode sim ser diferente do nome da conta. No exemplo real, claro, esse ID não seria nenhuma palavra compreensível, e sim uma cadeia de caracteres, mas para ficar mais fácil para nós, usamos um ID legível. E seguida, executamos a transferência. Nesse caso, vamos executar a transferência e vamos esperar um resultado.
Se esse resultado for true, é porque tudo deu certo e ele passou por todas, se o resultado for false, é porque ele caiu em algum lugar. Para testarmos, podemos vir no canto superior direito, no símbolo do triângulo verde, para rodar o nosso código. Embaixo, no terminal, veremos que ele retornará para nós false. Algo não está acontecendo, mas esse é o motivador do nosso curso.
Dessa forma que sabemos fazer até agora, sabemos que algo deu errado, mas não sabemos o que deu errado. Isso que é o complicador que vamos resolver usando os conhecimentos de exceção. Para isso, precisamos falar realmente do que são exceções. Te vejo mais para frente. Até mais.
Agora que já conhecemos o projeto que vai servir de base para esse curso, é bem importante conversarmos sobre aquele lance de Caminho Feliz, de programarmos apenas o caminho que funciona completamente, sem pensar nos erros e nos problemas que podem aparecer. Vamos falar sobre Caminho Feliz.
Imagine, por exemplo, que você desenvolveu um aplicativo ou uma aplicação Mobile que tem um cadastro, e você quer fazer algo simples, mandar esse cadastro para um banco de dados Web. Você programou só o caminho feliz, só o caminho que funciona, com o código perfeito para mandar isso para a Web. Depois você publicou e isso já está na mão da pessoa usuária que vai usar.
Então, digamos que você está programando em Fortaleza e a pessoa já está usando em São Paulo. Então você já não tem mais controle disso. Vamos ver qual foi o caminho que você programou onde tudo deu certo. Você fez um preenchimento de formulário. Então você criou um formulário para essa pessoa preencher e ela preencheu tudo perfeitamente sem nenhum problema.
Depois você fez um registro local, ou seja, todas essas informações você registrou primeiro no smartphone da pessoa e isso também funcionou perfeitamente. Com o registro local feito, você fez uma requisição via API para o banco de dados da nuvem, onde você quer registrar essas informações, e tudo ocorreu perfeitamente bem.
Então você foi desde o começo, onde você fez a aplicação que vai ter o cadastro, e ela foi registrada tanto localmente, quanto na base na Web com sucesso. Porém, te pergunto, sempre tudo vai dar certo assim? Tudo vai ser tão perfeito? Na verdade, nem sempre tudo vai dar certo, e é bem interessante notarmos isso. Podemos fazer isso até mesmo com esse exemplo.
Por exemplo, durante o preenchimento do formulário a pessoa pode ter inserido caracteres que são inválidos, que você não desejaria. Ela pode ter colocado um tipo incorreto, como, por exemplo, colocar números no nome. Não faz tanto sentido. Ela pode ter colocado uma data que é inválida, por exemplo, ter colocado o dia 31 no mês de setembro que sabemos que não tem.
Esses são caminhos que não terminam no final feliz do banco de dados salvo na Web, e sim que seguem por uma tangente, que temos que considerar. Digamos que tudo deu certo com o preenchimento de formulários. Imagine agora na hora de registrar localmente, de deixar isso guardado no smartphone da pessoa. O espaço de armazenamento, principalmente nos smartphones é limitado.
Às vezes você vai ter poucos megabytes para trabalhar, e registrar algo que seja maior do que isso vai gerar uma falta de espaço. Outra coisa é a falta de memória. Os smartphones cada vez tem mais memória RAM, mas eles têm uma limitação. Assim como em computadores, o seu aplicativo vai poder usar só uma fração daquela memória. Caso ele esteja com muitas coisas abertas, isso pode não dar certo.
Outra coisa que pode acontecer também é o acesso negado, caso a pessoa dona daquele smartphone ainda não liberou que sua aplicação possa gravar e ler da própria memória, como o SD, ou a memória interna do smartphone. Essas são as coisas que poderiam acontecer no registro local. Então já vai se abrindo uma árvore que vai além do Caminho Feliz.
Se tudo der certo com o registro local e formos tentar fazer essa requisição para Web, é aqui que você vai perceber que muitos problemas podem acontecer quando você está tentando fazer essa requisição para Web. Como, por exemplo, o smartphone estar sem Internet, um servidor que não responde, uma requisição inválida, porque foi montada de uma forma inválida ou porque o servidor não aceita esse tipo de requisição, e por aí vai. Os problemas são vários.
O que acontece se programarmos apenas o Caminho Feliz, apenas o caminho onde tudo dá certo, como estamos acostumados até agora a fazer? A verdade é que você terá programado em Fortaleza e a pessoa usará o aplicativo em São Paulo e vai dar um erro no aplicativo bem na cara da pessoa que está usando. O aplicativo vai fechar, vai crachar, como chamamos, e não vai ter nada que você vai poder fazer imediatamente, porque isso era algo que você deveria ter feito antes.
Você deveria ter tratado essas situações antes, justamente para não acontecer esses erros quando a pessoa já estiver usando, mas todos esses casos que citei são, de fato, erros? Vamos pensar no termo erro. Todos esses casos são erros? A verdade é que não, nem todos esses casos são erros. Muitas vezes os casos que vão fugir do Caminho Feliz não são necessariamente coisas que estão erradas, coisas que o seu código fez de errado.
E sim, só situações que podem acontecer e que não vamos levar elas para o Caminho Feliz, mas precisamos prevê-las e tratá-las. Essas situações são justamente para o termo que ouvimos muito na programação, chamada Exceções, justamente situações excepcionais. Então, falando delas, as exceções são coisas que sabemos que acontecerão e nos planejamos.
São aqueles caminhos, por exemplo, no aplicativo de banco que vamos construir, quando quem está enviando uma quantidade, tenta enviar uma quantidade que é maior do que a quantidade que ele tem em saldo. Então essa pessoa tenta enviar e isso será um comportamento que vai ser comum. É normal que os usuários cometam esse tipo de coisa, e precisamos prever. Porém isso não é erro, é uma exceção, é algo que é excepcional ao modelo padrão do nosso modelo de negócio.
Os erros são situações que não deveriam ter acontecido e que, muitas vezes, não temos controle sobre elas. Por exemplo, quando você tem que fazer uma operação que é para a Internet e o smartphone não está conectado na Internet. Isso não faz parte do seu modelo de negócios, mas também, se você não estiver preparado para lidar com isso, o seu aplicativo não fica tão amigável, tão funcional.
Vamos mais a fundo. O que é uma exceção na definição do Dart? Segundo a Dart Library Tour, "exceções são consideradas condições que você pode planejar e resolver depois". Então fala muito dessa imagem. Conhecemos essa placa de trânsito que você pode seguir em frente ou tomar uma tangente. A mesma coisa.
Você pode seguir em frente para o modelo perfeito que você definiu na sua resolução de problemas, no seu modelo de negócios, mas você também pode prever situações excepcionais. Mais uma vez usando o termo excepcional, para fazer tangente que não vão necessariamente quebrar o código, ou seja, fazer com que o aplicativo pare, mas que você trate e possa dar, por exemplo, uma tela de erro ou uma tela de mensagem amigável para quem estiver usando sem ser o programa fechando.
Alguns exemplos de exceções. O FormatException é bem famoso quando você, por exemplo, tenta converter um texto para data e você não segue um formato que aquela data esperaria, ou seja, o formato de dia, mês e ano ou o formato americano e por aí vai.
Vejam que isso é previsto. O TimeoutException é quando, por exemplo, você faz uma requisição para Web, mas poderia ser para várias outras coisas, e você define um tempo, "se não me responderem nesse tempo, vou dar um TimeoutException para não ficar esperando para sempre". O usuário nunca quer esperar para sempre, então o TimeoutException serve justamente para isso.
Mais uma vez, previsibilidade. O IOException para entrada, saída e suas exceções, afinal, nesse curso vamos aprender a fazer nossas próprias exceções para responder ao nosso modelo de negócios. E o que é um erro? Um erro, segundo a Dart-Core, representa uma falha no programa que a pessoa programadora deveria ter evitado. Eles são condições que não se esperava ou que não se planejou para.
Coisas que aconteceram e que não esperávamos, porque não pensamos a respeito, ou que até esperávamos, mas por má programação não nos planejamos, e gerou um erro que explodiu na cara da pessoa usuária. Exemplos de erros é o StackOverflow, por exemplo, que é quando fazemos uma chamada recursiva que estoura a nossa pilha prevista pelo sistema que está controlando o nosso programa, e isso gera um erro.
Veremos ele logo em seguida, porque ele é um erro bem famoso e é legal de falar dele. O OutOfMemoryError, como falei, principalmente se tratando de smartphones, vamos ter um acesso limitado a memória RAM. Então, se tiver vários aplicativos abertos, pode ser que o nosso aplicativo precise de mais RAM do que o que tem disponível, e dá esse OutOfMemoryError.
O ArgumentError, que é um erro tão famoso que o próprio Dart já faz uma checagem disso para você. É quando, por exemplo, você tem um parâmetro que esperava um inteiro como argumento e você passa como argumento um string. Sabemos que isso não funciona no Dart, e ele já deixa vermelho. Esse é o ArgumentError. Se ele não deixasse vermelho para nós, e tentássemos rodar, ele ia justamente dar um ArgumentError.
E o TypeError é quando, por exemplo, tentamos fazer uma conversão, só que ela não faz sentido. Por exemplo, tentar converter uma letra para um número. Não faz sentido, TypeError. Vamos ver alguns desses exemplos no código. Vamos abrir o IntelliJ. Vou clicar em "Project", na esquerda, para minimizarmos os diretórios e termos uma boa visualização do código. Vou clicar na barra para esconder o terminal. Vamos fazer basicamente dois erros.
Por enquanto, vamos pegar esse código que conversamos anteriormente, e que teve aquele problema de que, caso rodemos e ele gere uma situação que não é a perfeita, não conseguimos visualizar qual é essa situação, porque estávamos lidando de uma forma de que não usa exceções. Por enquanto, vou comentá-lo, porque estamos falando mais dessas coisas mais teóricas. Então vou dar "Ctrl + barra". É um atalho. Ele comenta toda a linha selecionada.
Vou criar uma nova main, void main() {}
, só para fazermos alguns testes. O primeiro teste é tentar converter alguma coisa que não faz sentido. Vou fazer, double amount
, por exemplo, que seria a quantidade, o saldo ou o que for, e fazer double amount = double.parse()
, e vou tentar converter isso. Nem vou colocar Ricarth
, porque não faria sentido nenhum eu converter para double.
Mas vou colocar, por exemplo, 25,33
, que sabemos que isso tem que ser um ponto e não uma vírgula, para que essa conversão funcione, double amount = double.parse("25,33");
. Então vou salvar, clicar no botão verde de rodar no canto superior direito. Embaixo, vai abrir o terminal, onde ele vai dar um erro dizendo justamente "FormatException: Invalid double".
O que mais? Outro erro que podemos provocar é o famoso StackOverflowError. No caso, essa foi a Exception, vamos gerar um erro. Vamos criar, recursiveFunc() {}
. E o que ela faz? Ela pode imprimir um contador, por exemplo, print(count);
. Vou precisar receber esse parâmetro como contador, então, recursiveFunc(int count) { print(count); }
.
E vou chamar ela mesma, recursiveFunc(count + 1);
. Vou passar count + 1
para ver até quantas repetições o sistema operacional nos deixa repetir essa função recursiva. Então é bem simples: tenho uma função que vai printar o número que ela receber e vai chamar ela mesma passando mais um número. Precisa inicializar ela, então, recursiveFunc(1);
começando em 1 e vamos ver até onde vai.
Vou comentar a linha 28, que é a double amount, onde fazemos a conversão que não dá certo. Só para ele não dar o erro, nós damos o outro erro. Vamos rodar e ver o que acontece. Ele pensou bastante, porque primeiro ele fez o quanto ele pode dessa pilha recursiva. Então ele começa no 1, 2, 3 e assim por diante. Ele vai até 21895 funções sendo abertas uma depois das outras.
Então ele dá um erro, que é o famoso StackOverflowError. Stack significa pilha, e essa pilha que estourou é a pilha de execução. A pilha de execução é um conceito extremamente importante quando queremos falar de exceções. Então vamos falar sobre ela mais à frente. Te vejo lá.
O curso Dart: lidando com Exceptions e Null Safety possui 219 minutos de vídeos, em um total de 53 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:
Impulsione a sua carreira com os melhores cursos e faça parte da maior comunidade tech.
1 ano de Alura
Assine o PLUS e garanta:
Formações com mais de 1500 cursos atualizados e novos lançamentos semanais, em Programação, Inteligência Artificial, Front-end, UX & Design, Data Science, Mobile, DevOps e Inovação & Gestão.
A cada curso ou formação concluído, um novo certificado para turbinar seu currículo e LinkedIn.
No Discord, você tem acesso a eventos exclusivos, grupos de estudos e mentorias com especialistas de diferentes áreas.
Faça parte da maior comunidade Dev do país e crie conexões com mais de 120 mil pessoas no Discord.
Acesso ilimitado ao catálogo de Imersões da Alura para praticar conhecimentos em diferentes áreas.
Explore um universo de possibilidades na palma da sua mão. Baixe as aulas para assistir offline, onde e quando quiser.
Acelere o seu aprendizado com a IA da Alura e prepare-se para o mercado internacional.
1 ano de Alura
Todos os benefícios do PLUS e mais vantagens exclusivas:
Luri é nossa inteligência artificial que tira dúvidas, dá exemplos práticos, corrige exercícios e ajuda a mergulhar ainda mais durante as aulas. Você pode conversar com a Luri até 100 mensagens por semana.
Aprenda um novo idioma e expanda seus horizontes profissionais. Cursos de Inglês, Espanhol e Inglês para Devs, 100% focado em tecnologia.
Transforme a sua jornada com benefícios exclusivos e evolua ainda mais na sua carreira.
1 ano de Alura
Todos os benefícios do PRO e mais vantagens exclusivas:
Mensagens ilimitadas para estudar com a Luri, a IA da Alura, disponível 24hs para tirar suas dúvidas, dar exemplos práticos, corrigir exercícios e impulsionar seus estudos.
Envie imagens para a Luri e ela te ajuda a solucionar problemas, identificar erros, esclarecer gráficos, analisar design e muito mais.
Escolha os ebooks da Casa do Código, a editora da Alura, que apoiarão a sua jornada de aprendizado para sempre.