Dart 01 – Introdução

Este artigo é a parte [part not set] de 3 na série Dart

Esta série proporcionará uma breve introdução à linguagem Dart, abordando seus conceitos básicos sem adentrar profundamente nos detalhes mais intricados.

Dart, originalmente conhecida como Dash, é uma linguagem desenvolvida pela Google e foi lançada durante a GOTO Conference de 2011. Seu propósito inicial era substituir o JavaScript, sendo concebida com certa semelhança ao Java e com a capacidade de compilar para código JavaScript. Atualmente, o Dart é amplamente utilizado para criar aplicativos móveis, aplicações web e servidores. Ele foi projetado com ênfase na eficiência, rapidez, segurança e facilidade de aprendizado.

Em resumo, alguns pontos-chave do Dart incluem:

  • Linguagem Orientada a Objeto: Segue os princípios de encapsulamento, herança e polimorfismo.
  • Tipagem Flexível: Oferece suporte a uma abordagem flexível para a tipagem, permitindo tanto a tipagem inferida quanto a tipagem explícita.
  • Compilação Just-in-Time (JIT) e Ahead-of-Time (AOT): Utiliza duas estratégias de compilação, proporcionando um ciclo de desenvolvimento rápido e desempenho máximo em produção.
  • Gerenciamento de Memória Automático: Possui um coletor de lixo automático, simplificando o processo de desenvolvimento e tornando-o mais seguro.
  • Concorrência: Suporta programação assíncrona e concorrente por meio de futuros (Futures) e streams, facilitando operações como chamadas de API e acesso a bancos de dados.
  • Ecossistema Flutter: Embora o Dart possa ser usado de forma independente, destaca-se como a linguagem principal no Flutter, um framework de desenvolvimento multiplataforma para criação de interfaces de usuário. Flutter permite o desenvolvimento de aplicativos nativos para iOS, Android, Web e desktop a partir de um único código base.

Embora o Flutter não seja o foco deste texto, planejo escrever artigos adicionais sobre ele no futuro.

Editor para o Dart

Existem diversas IDEs disponíveis com suporte ao Dart, dentre elas sugiro o Visual Studio Code com a extensão Dart, e Flutter se pretende programar em Flutter, e também o Android Studio, se pretende criar aplicativos para Android, iOS, Linux… o recomendo para um ecossistema mais bem integrado.

No entanto, para este primeiro momento sugiro apenas o site DartPad. Nele você pode escrever e executar seu código Dart sem a necessidade de instalar nada em sua máquina, tudo via web.

Um Programa Básico

A estrutura básica de um programa em Dart segue o exemplo do código a seguir:

void main() {
  // This is a comment
  String name = 'Albert';
  print('Hello $name!');
}

Todo aplicativo Dart deve possuir ao menos uma função ‘main‘ sem retorno, void, linha 1. Comentários segue o mesmo padrão de muitas linguagens, entre elas o Go. Barras duplas (//) para uma linha de comentário ou (/* .... */) para múltiplas linhas, linha 2.

Declaração de variáveis são sempre precedidas do tipo (String, int, double, …) nome da variável e, eventualmente, sua inicialização como na linha 3.

O conteúdo da variável pode ser acessada por meio do caractere ‘$‘ seguido do nome da variável, quando entre aspas simples ou duplas, para compor um texto de saída, ou simplesmente fazendo um print(nome), para imprimir apenas o conteúdo da variável.

Cada linha de comando deve terminar com um ponto e vírgula e todo o corpo da função deve estar entre chaves. Este código irá apenas imprimir um “Hello Albert!” quando executado.

A seguir decorro sobre alguns aspectos importantes que se deve ter em mente quando desenvolver em Dart.

Tipos de Dados e Variáveis

Dart é uma linguagem fortemente tipada, restringindo seu tipo ao declarado por inferência ou explicitamente. Há duas formas de declarar variáveis em Dart, de forma implícita, usando a keyword var:

void main() {
  var name0 = 'Joana';
  var name1 = 34.56;
    
  print(name0.runtimeType);
  print(name1.runtimeType);
}

Desta forma o Dart irá inferir o tipo da variável em tempo de compilação. De forma explicita, declarando o tipo da variável:

void main() {
  String name0 = 'Joana';
  double cost = 34.56;
  int age = 12;
    
  print(name0.runtimeType); // imprime: String
  print(cost.runtimeType);  // imprime: double
  print(age.runtimeType);   // imprime: int
}

Pessoalmente prefiro a declaração explícita, com o tipo da variável declarada juntamente, deixando o var para situações em que o seu tipo é de pouca relevância para o código, ou outros casos muito especiais.

Este código apenas imprime os tipos das variáveis. Como pode ser observado, no Dart toda variável são objetos, e não somente as variáveis como também as funções, listas, conjuntos, …

Todo conteúdo de uma variável é um objeto

Isto já está implícito nos exemplos acima, onde o atributo runtimeType é checado para retorna o tipo da variável. Pois bem, tudo que se põem em uma variável no Dart é um objeto, uma instância de uma classe. Isto serve para realmente tudo, seja um inteiro, string, função, null… e todos estes objetos herdam da classe fundamental Object. Como se pode perceber a Orientação a Objeto está muito incrustada no Dart.

A classe Object possui alguns atributos importantes que são:

  • hashCode – um inteiro único (hash code) para o objeto;
  • runtimeType – o tipo do objeto;

O método relevante aqui é o

  • toString() – que retorna uma representação string do conteúdo armazenado.

Este último método é muito sobre escrito para criar uma saída personalizada para uma classe.

Variável Nullable e No-nullable

Em princípio as variáveis no Dart são declaradas como Non-nullable, ou seja, não podem assumir o valor nulo. Isto significa que elas devem ser inicializadas antes de serem acessadas, ou irá gerar um erro. Observe o código a seguir:

void main() {
  int age;
  // age = 10;
  print(age);
}

O código compila sem problemas mas ao executar gera um erro na linha 4 já que a variável age não foi inicializada. Removendo o comentário da linha 3 a variável age será inicializada e o código irá executar sem problemas.

No entanto, uma variável em Dart pode conter o valor null, mesmo um inteiro, desde que esta seja declarada como nullable. Para isto basta adicionar um ponto de interrogação (?) após o tipo da variável na declaração:

void main() {
  int? age;
  print(age);
}

Agora o código irá rodar sem a necessidade de iniciar a variável age, visto que ela é inicializada em tempo de compilação com o valor null.

Null Safety

Observe que a age não pode ser empregada em operações matemáticas comuns com inteiros, visto que o seu valor pode ser null. Variáveis nullable devem ser testadas antes de serem empregadas em operações específicas de um tipo.

Observe o código a seguir.

main() {
  String? name;
  String surname = 'Einstein';
  
  // name = 'Albert';
  
  String fullName = name + ' ' + surname;
  
  print(fullName);
}

Se tentar executá-lo no DartPad isto irá gerar o erro:

Error compiling to JavaScript:
Info: Compiling with sound null safety.
lib/main.dart:7:26:
Error: Operator '+' cannot be called on 'String?' because it is potentially null.
  String fullName = name + ' ' + surname;
                         ^
Error: Compilation failed.

Isto ocorre porque a variável name é potencialmente null (String?) e por isto não é suportada pelo operador adição com String. Da forma como está o código nem mesmo compila, gerando o mesmo error Compiling with sound null safety.

Se quiser compilar este código execute o comando abaixo no seu terminal:

Bash

alves@arabel:~$ dart compile exe fullname.dart
Info: Compiling with sound null safety.
prog01.dart:10:26: Error: Operator '+' cannot be called on 'String?' because it is potentially null.
String fullName = name + ' ' + surname;
^
Error: AOT compilation failed
Generating AOT kernel dill failed!

Existe uma forma de forçar esta operação dizendo ao compilador aceitar que name não é nula. Neste caso se deve adicionar uma exclamação ao final do nome da variável, como name! Adicionando uma exclamação ao name

main() {
  String? name;
  String surname = 'Einstein';
  
  // name = 'Albert';
  
  String fullName = name! + ' ' + surname;
  
  print(fullName);
}

Agora o código pode ser compilado, mas ainda vai gerar um erro a ser executado, já que name ainda permanece sem conteúdo.

Bash

alves@arabel:~$ dart compile exe fullname1.dart
Info: Compiling with sound null safety.
Generated: /home/rudson/Documents/Studien/Dart/nullable/prog01.exe
alves@arabel:~$ ./fullname1.exe
Unhandled exception:
Null check operator used on a null value
0 _delayEntrypointInvocation. (dart:isolate-patch/isolate_patch.dart:296)
1 _RawReceivePort._handleMessage (dart:isolate-patch/isolate_patch.dart:189)

Removendo o comentário da linha 5 o código irá compilar com um warning, visto que em situações simples como esta o compilador percebe que name não é mais null, perdendo o sentido a exclamação na variável name. Este não é o uso adequado de uma variável nullable, mas serve com o propósito de demonstração. Futuramente vou estender mais a discussão deste assunto.

O operador ?? permite fazer uma substituição na variável caso ela seja nula. O código anterior pode ser corrigido na forma:

oid main() {
  String? name;
  String surname;

  surname = 'Einstein';
  
  // name = 'Albert';
  
  String fullName = name?? 'Albert';
  fullName += ' ' + surname;

  print(fullName);
}

Agora a linha 9 inicia fullName com name, mas caso ele seja nulo será inciado com 'Albert'.

Keyword late

O Dart possui uma forma de “assinar um contrato” com o desenvolvedor de que uma variável será inicializada antes de ser acessada. Isto é feito com a keyword late. Ela declarar que uma variável no-nullable será iniciada posteriormente, antes de ser acessada.

late String msg;

void main() {
  msg = 'Devo ser iniciada antes de ser acessada';
  
  print(msg);
}

Caso a linha 4 seja comentada o código irá gerar uma exceção LateInitializationError, já que msg não é inicializada. O uso do late determina que a variável declarada tem de ser inicializada antes de ser acessada.

Neste código a utilidade do late parece pouco relevante, mas isto se tornará muito importante na criação de classes onde certos atributos devem ser declarados como no-nullable mas somente são iniciados a posteriori no código por questões de sua lógica de construção. É minha intenção retomar esta discussão adiante.

Keywords final e const

Sempre que o valor de uma variável não for alterado ao longo do código, ela deve ser declarada como uma constante. No Dart, isso é realizado usando as palavras-chave final ou const. Ambas declaram constantes, mas possuem uma pequena diferença. Uma variável declarada como const será tratada como uma constante em tempo de compilação. Por outro lado, uma variável do tipo final é tratada como uma constante em tempo de execução. Isso significa que uma variável do tipo final será alocada em memória, e todas as consultas ao seu valor serão feitas a essa alocação. Em contraste, uma const é substituída ao longo do código pelo seu valor constante.

O uso dessas palavras-chave é simples: basta adicioná-las antes do tipo na declaração, como exemplificado no código a seguir:

void main() {
  const pi = 3.14156;
  double raio = 5.0;
  
  final area = pi * raio * raio;
  final circunferencia = 2 * pi * raio;
 
  print('Circunferência: $circunferencia');
  print('Área: $area');
}

No código acima, pi é uma constante, pois o valor é conhecido em tempo de compilação e não muda. Em contraste, area e circunferencia são variáveis finais, pois seus valores são calculados em tempo de execução e não são alterados após a atribuição durante a execução do código.

Tipos Básicos

O Dart apresenta um conjunto bastante restrito de tipos básicos de variáveis quando comparado a linguagens tradicionais. Por exemplo, nos tipos numéricos, existem apenas dois tipos: int para inteiros e double para números de ponto flutuante, ambos pertencentes à classe num.

Uma curiosidade, e até mesmo uma controvérsia, sobre o Dart é que apenas os tipos básicos int, double, bool e num são escritos em letras minúsculas, enquanto os demais, como List, Set e Maps, são todos inicializados com letras maiúsculas.

No Dart, há uma orientação para que as classes sejam declaradas sempre iniciando com letras maiúsculas; no entanto, isso não é seguido como padrão nos tipos int, double, bool e num. Pelo que pude verificar em fóruns na internet, a escolha de usar letras minúsculas nesses tipos básicos é para que os programadores em Java se sintam mais confortáveis ao migrar para o Dart, já que os tipos primitivos correspondentes do Java são declarados com letras minúsculas.

Entretanto, lembre-se de que no Dart não existem tipos primitivos; int, double, bool e num são todos classes que representam esses tipos. Há uma discussão sobre o assunto em uma thread no Stack Overflow (Dart Class starting with uppercase (p.e. int, bool, num, … should be Int, Bool, Num, … !?)) e pessoalmente concordo com o Muka de que a nomenclatura deveria priorizar uma padronização da linguagem em vez de costumes herdados de outras linguagens. No entanto, compreendo a escolha dos desenvolvedores, embora ache que, em algum momento, eles terão que reconsiderar.

Tipo int

Em plataformas de 64 bits, um int pode variar de -2⁶³ até 2⁶³-1. Em aplicativos web, um int pode variar de -2⁵³ até 2⁵³-1.

Tipo double

São números de dupla precisão de 64 bits, conforme as especificações IEEE754.

Tanto int como double são subtipos da classe num, possuindo praticamente os mesmos métodos e operadores, com algumas pequenas particularidades pontuais, como na divisão.

void main() {
  int count = 12;
  double number = 12.5;
  
  count ++; // count = 13 
  number++; // number = 13.5
  
  count -= 25; // count = -12
  number += 12; // number = 25.5
  
  count ~/= 3; // count = -4
  number /= 3; // number = 8.5
  
  print(number.ceil()); // retrona 9
  print(count.abs()); // retorna 4
}

No código acima, apenas os operadores de divisão não são compartilhados por int e double. Para obter um resultado inteiro, apenas o operador de divisão inteira (~/) pode ser aplicado, proporcionando um retorno inteiro. Já para um double, o operador de divisão (/) retorna um double, mesmo que o resultado da divisão seja inteiro.

Tipo String

Um objeto do tipo String em Dart é uma sequência de código UTF-16 declarados entre aspas simples ou duplas. O código a seguir apresenta alguns métodos da classe String.

void main() {
  String firstName = 'albert';
  String lastName = 'einstein';
 
  String name = '${firstName} ${lastName}';
  
  print(name.toUpperCase());    // ALBERT EINSTEIN
  print(name.length);           // 15
  print(name.runes);            // (97, 108, 98, 101, 114, 116, 32, 101, 105, 110, 115, 116, 101, 105, 110)
  print(name.contains('ber'));  // true
  print(name.indexOf('ins'));   // 8
  print(name.lastIndexOf('i')); // 13
  print(name.split(' '));       // [albert, einstein]
}

Tipo Booleano e Operadores

Dart usa a keyword bool para declarar booleanos, com os valores true e false. Dart suporta uma gama de operadores associativos, com suas respectivas precedências, apresentados na tabela a seguir:

DescriçãoOperadorAssociatividade
unitário posfixoexpr++     expr--     ()     []     ?[]     .     ?.     !Nenhum
unário prefixo-expr     !expr     ~expr     ++expr     --expr     await exprNenhum
multiplicativo*     /     %     ~/Esquerda
aditivo+     -Esquerda
deslocamento de bit<<     >>     >>>Esquerda
bit a bit AND&Esquerda
bit a bit XOR^Esquerda
bit a bit OR|Esquerda
teste relacional e de tipo>=     >     <=     <     as     is     is!Nenhum
igualdade ==     != Nenhum
E lógico&&Esquerda
OU lógico||Esquerda
Se null??Esquerda
Condicionalcondicional ? <expr1 se verdadeiro> : <expr2 se falso>Direita
Cascata..     ?..Direita
Atribuição =     *=     /=     +=     -=     &=     ^=     etc.Direita

Função assert

A função assert é usada para realizar verificações durante o desenvolvimento para garantir que as suposições sobre o seu código sejam verdadeiras. Essas verificações são destinadas a serem desativadas em ambientes de produção para garantir o melhor desempenho possível.

Sua sintaxe básica é a seguinte:

assert(expresão, mensagem);
  • expressão: É uma condição que você espera ser verdadeira. Se a expressão for falsa, um erro é lançado.
  • mensagem: Uma mensagem opcional que é exibida se a expressão for falsa. Isso pode ajudar a identificar o motivo da falha da assert.

Por exemplo:

void main() { 
  int valor = 10; 
  assert(valor > 0, 'O valor deve ser maior que zero'); // Restante do código... 
}

No exemplo acima, se o valor de valor não for maior que zero, a assert será acionada, e a mensagem “O valor deve ser maior que zero” será exibida.

É importante notar que as verificações assert são ignoradas em ambientes de produção (quando o código é compilado com o modo de produção). Portanto, você pode usá-las livremente durante o desenvolvimento para capturar possíveis erros, mas elas não terão impacto no desempenho do seu aplicativo em produção.

Alguns testes com Operadores

Os códigos a seguir foram retirado diretamente da documentação do Dart. O primeiro apresentando alguns testes com a função assert em operadores aritméticos:

void main() {
  assert(2 + 3 == 5);
  assert(2 - 3 == -1);
  assert(2 * 3 == 6);
  assert(5 / 2 == 2.5); // Result is a double
  assert(5 ~/ 2 == 2); // Result is an int
  assert(5 % 2 == 1); // Remainder
  assert('5/2 = ${5 ~/ 2} r ${5 % 2}' == '5/2 = 2 r 1');
}

Todo o código passa sem gerar exceção, visto que todas as expressões são verdadeiras. O próximo código apresenta exemplos dos operadores unitários prefixo e posfixo (pós-fixado).

void main() {
  int a;
  int b;

  a = 0;
  b = ++a; // Increment a before b gets its value.
  assert(a == b); // 1 == 1

  a = 0;
  b = a++; // Increment a AFTER b gets its value.
  assert(a != b); // 1 != 0

  a = 0;
  b = --a; // Decrement a before b gets its value.
  assert(a == b); // -1 == -1

  a = 0;
  b = a--; // Decrement a AFTER b gets its value.
  assert(a != b); // -1 != 0
  
  // tests of relational operators
  assert(2 == 2);
  assert(2 != 3);
  assert(3 > 2);
  assert(2 < 3);
  assert(3 >= 3);
  assert(2 <= 3);
  
  // OR, AND, NOT
  int col = 0;
  assert(!(2 == 3) && (col == 0 || col == 3));
}

A precedência é da esquerda para a direita, por isto a expressão da linha 6 deixa as variáveis a e b com o mesmo resultado, visto que o incremento de a ocorre antes da atribuição a variável b. O mesmo não ocorre na linha 10, já que a atribuição de a para b ocorre após o incremento da variável a. O mesmo se repete das linhas 15 e 18 com o operador de decremento.

As linhas 21 à 27 mostram alguns testes relacionais, e 30-31 uns teste com os operadores negação (!), E (&&) e OU (||).

O código a seguir apresenta algumas operações bit a bit:

void main() {
  final a = 0x22;
  final b = 0x0f;

  print(a >> 4);

  assert((a & b) == 0x02); // AND
  assert((a & ~b) == 0x20); // AND NOT
  assert((a | b) == 0x2f); // OR
  assert((a ^ b) == 0x2d); // XOR
  assert((a << 4) == 0x220); // Shift left
  assert((a >> 4) == 0x02); // Shift right
  assert((-a >>> 4) > 0); // Unsigned shift right
}

Tipo dynamic e Object

É possível declarar uma variável de tipagem fraca no Dart declarando-a do tipo dynamic ou Object. O código a seguir mostra o emprego do dynamic com os atributos apresentados acima e o método toString em ação.

void main() {
  dynamic value;
  // Object  value;
  // var value;
  
  value = 'Fernanda';
  print('Type:     ${value.runtimeType}');
  print('toString: ${value.toString()}');
  print('hashCode: ${value.hashCode}');
  print('');
  
  value = <int>[1, 2, 3];
  print('Type:     ${value.runtimeType}');
  print('toString: ${value.toString()}');
  print('hashCode: ${value.hashCode}');
  print('');
  
  value = true;
  print('Type:     ${value.runtimeType}');
  print('toString: ${value.toString()}');
  print('hashCode: ${value.hashCode}');
  print('');
  
  value = funcao;
  print('Type:     ${value.runtimeType}');
  print('toString: ${value.toString()}');
  print('hashCode: ${value.hashCode}');
  print('');
  
  value = Pessoa;
  print('Type:     ${value.runtimeType}');
  print('toString: ${value.toString()}');
  print('hashCode: ${value.hashCode}');
  print('');
}

void funcao() {
  print('Sou uma função sem retorno');
}

class Pessoa {
  String? nome;
  int? idade;
  
  @override
  String toString() {
    return 'Nome: $nome  Idade: $idade';
  }
}

Este código cria uma variável value do tipo dynamic, a qual pode conter diferentes tipos. O código se comportará da mesma forma trocando a declaração de dynamic na linha 2 pela linha 3 ou 4. A última forma de declaração, linha comentada 4, não é aconselhável o seu uso, no entanto irá gerar o mesmo resultado.

A saída deste código é mostrado a seguir:

Type:     String
toString: Fernanda
hashCode: 162918149

Type:     JSArray<int>
toString: [1, 2, 3]
hashCode: 694773128

Type:     bool
toString: true
hashCode: 519018

Type:     () => void
toString: Closure 'funcao'
hashCode: 986877998

Type:     _Type
toString: Pessoa
hashCode: 308163971

Cada novo tipo passado para a variável value retorna um novo hashCode. Isto significa que o código cria uma nova variável a cada nova resignação de tipo diferente, realocando uma nova instância na memória.

Uma variável declarada do tipo dynamic é simplesmente uma variável com a verificação de estática desabilitada. Isto significa que a variável além de aceitar um tipo qualquer ela também vai aceitar qualquer operação, pois esta verificação é desabilitada em tempo de compilação.

O código a seguir ilustra melhor isto.

void main() {
  dynamic a;
  dynamic b;
  
  a = '67';
  b = 3;
  
  print(a/b);
}

Em tempo de compilação a linha 8 será compilada como se a e b fossem instâncias de classes que suportem qualquer operação, inclusive a divisão real, no entanto no momento da execução isto irá gerar um erro nesta divisão:

Uncaught TypeError: B.JSString_methods.$div is not a functionError: TypeError: B.JSString_methods.$div is not a function 

Declarando as variáveis como Objetc neste código o compilador alerta a operação e não compila:

Error compiling to JavaScript:
Info: Compiling with sound null safety
lib/main.dart:8:10:
Error: The operator '/' isn't defined for the class 'Object'.
 - 'Object' is from 'dart:core'.
  print(a/b);
         ^
Error: Compilation failed.

Isto ocorre porque a classe Objetc não aceita a operação de divisão e não porque o erro da divisão de uma string por um inteiro é identificado. Entretanto, isto evita erros inesperados no código.

Observe que o código a seguir iria compilar e funcionar com o tipo dynamic, mas não com o tipo Object:

void main() {
  dynamic a;
  dynamic b;
  
  a = 67;
  b = 3;
  
  print(a/b);  // imprime: 22.333333333333332
}

O fato é que se deve tomar muito cuidado em trabalhar com variáveis do tipo Object e ainda mais do tipo dynamic. O melhor é evitar usar estes tipos no código e sempre que possível optar pelo uso do tipo Object (para variáveis no-nullable) ou Object? na declaração de variável mutáveis ao invés do dynamic.

Tipos Object e dynamic tem a sua utilidade e, definitivamente, estas peças de código não são a forma adequada de empregá-los. Em momento mais oportuno devo retomar esta discussão e fazer um melhor emprego destes tipos.

Listas

No Dart arrays são objetos do tipo List, onde seus valores são declarados entre colchetes. A sintaxe da declaração de uma Lista é apresentada a seguir:

List<tipo> lista1 = [ e1, e2, e3, ...];
var lista2 = [e1, e2, e3, ...];

Neste caso o “<” e “>” fazem parte da declaração. No caso a lista2 terá o tipo declarado por inferência, em tempo de compilação.

O código a seguir ilustra diferentes formas de declarar de listas:

void main() {
  List<String> nomes = ['Roberto', 'Sandra', 'Fernanda', 'Cláudio'];
  List<int> numeros = [1,2,3,4,5,6];
  List<Object> objetos = [true, 'Albert', 12, 6.55, sayHello, nomes];
  
  var nomes1 = ['Roberto', 'Sandra'];
  var numeros1 = <int>[1,2,3];
}

As declarações nas linhas 2 à 4 são a forma mais empregada de declaração. A linha 6 o tipo da lista é determinado pelo compilador, já a linha 7 a lista explicitamente criada e apontada para numeros1.

O código traz mais alguns aspectos de listas em Dart:

void main() {
  List<int> a = [1,2,3,4];
  var b = [5,6,7,8];
  
  List<int> c = [...a, ...b];
  
  List<int> d = [];
  d.addAll(c);
  
  List<int> e = List<int>.of(d.map((x) => x+10));
  
  List<int> f = d;
  
  List<int> g = a + b;
  
  a.first = 11;
  b[0] = 55;
  d[4] = 55;
  g.last = 88;
  
  print('a: $a'); // a: [11, 2, 3, 4]
  print('b: $b'); // b: [55, 6, 7, 8]
  print('c: $c'); // c: [1, 2, 3, 4, 5, 6, 7, 8]
  print('d: $d'); // d: [1, 2, 3, 4, 55, 6, 7, 8]
  print('e: $e'); // e: [11, 12, 13, 14, 15, 16, 17, 18]
  print('f: $f'); // f: [1, 2, 3, 4, 55, 6, 7, 8]
  print('g: $g'); // g: [1, 2, 3, 4, 5, 6, 7, 88]
}

Na linha 5 a lista c é criada a partir dos elementos das listas a e b. O operador ‘...a‘ expande os elementos da lista, no caso da lista a.

A linha 8 faz emprega o método addAll() para copiar os elementos da lista c em d, gerando assim listas independentes.

A linha 10 é empregado o construtor List<int>.of() para criar uma lista com o mapeamento dos elementos da lista d adicionado 10 a cada elemento. O método map() da lista gerar um iterável que alimenta o construtor of para gerar a lista e. Os construtores estão disponíveis apenas na classe padrão e não nos seus objetos instanciados, por isto a necessidade de usar o List<int>.of() para utilizá-lo.

Nas linha 12 é explorado um dos construtores da classe List, criando uma lista de inteiros de 12 elementos todos zeros. Nas linhas 14 e 25 são aplicados outros dois métodos da classe List, addAll() e removeWhere(). Outros muitos construtores, métodos e atributos bastantes úteis são encontrados na documentação: List.

A igualdade da linha 13 faz com que a lista a e c apontem para os mesmo elementos na memória, portanto são a mesma lista. Para gerar uma cópia pode usar o código comentado nas linhas 19 à 22. Na linha 19 o operados ...a retorna os elementos de a e concatena aos elementos de b, ...b. Isto gera uma nova lista com os elementos de a e b em c.

A linha 14 cria a lista g com o operador adição aplicado às listas de a e b.

As linhas 16 à 19 são apenas para gerar alguns marcadores para identificar as listas impressas. Isto permite observar que a lista f e d são as mesmas, ou seja, a igualdade da linha 12 passa uma referência para a lista f apontar para os elementos da lista d.

Os resultados destas operações estão apresentados nos comentários à frente dos prints das linhas 21 à 27. Observe que a menos das listas f e d, que compartilhem os mesmos elementos, as demais são cópias independentes dos elementos das listas iniciais, a e b.

Outros aspecto interessante das listas em Dart são os atributos first e last que dão acesso, de leitura e escrita, ao primeiro e último elemento da lista, respectivamente. Estes atributos foram usados para alterar os elementos das listas nas linhas 16 e 19.

Conjuntos

Sets em Dart é uma coleção de objetos únicos de um dado tipo. A declaração de um Set é semelhante a de uma List mas usando chaves no lugar de colchetes, como apresentado na sintaxe abaixo:

Set<tipo> setName = {e1, e2, e3, ...};
var setName = <tipo>{e1, e2, e3, ...};
var setName = {e1, e2, e3, ...};

O código a seguir inicia um Set de ambientes.

void main() {
  Set<String> ambientes = {'sala', 'quarto', 'cozinha'};
  // var ambientes = {'sala', 'quarto', 'cozinha'};
  // var ambientes = <String>{'sala', 'quarto', 'cozinha'};
  
  ambientes.add('lavabo');              // adiciona 'lavabo'
  print(ambientes);                     // {sala, quarto, cozinha, lavabo}
  print(ambientes.contains('varanda')); // false
  print(ambientes.elementAt(2));        // cozinha
  
  ambientes.add('sala');                // não faz nada pois 'sala' já existe no set ambientes
  print(ambientes);                     // {sala, quarto, cozinha, lavabo}
}

As linhas 3 e 4 são outras formas de declarar o mesmo Set de ambientes. As linhas 6 à 9 explora alguns métodos da classe Set, mas para mais informações o manual em Set-class. Observe que a linha 11 não adiciona uma segunda ‘sala‘ ao Set, pois este é um conjunto de elementos únicos.

Maps

Como em outras linguagens Maps são conjuntos de pares chave e valor onde, no Dart, tanto as chaves como os valores tem de ser de um tipo declarado. E também, com esperado, as chaves são um Set, ou seja, um conjunto de elementos únicos.

Maps são declarados como a sintaxe a seguir:

Map<type_key, type_value> mapName = {key1: value, key2: value2, ...}

Abaixo um código simples com o Map de String x double:

  Map<String, double> pesos = {
    'Carlos': 75.3, 
    'Roberta' : 65.7, 
    'José' : 98.3,
  };
  // var pesos = {'Carlos': 75.3, 'Roberta' : 65.7, 'José' : 98.3};
  // var pesos = <String, double>{'Carlos': 75.3, 'Roberta' : 65.7, 'José' : 98.3};
  
  pesos['Flávia'] = 65.7;
  pesos['Carlos'] = 78.2;
  print(pesos);
  print(pesos.keys);
  print(pesos.containsKey('Renata'));
  print(pesos.containsValue(65.7));

O código acima apresenta um Map de Strings x double com os nomes de pessoas como chave e os pesos como valor. As linhas 6 e 7 são formas alternativas para iniciar um Map. A linha 9 adiciona o valor 65.7 à chave ‘Flávia‘, e a linha 10 altera o peso de ‘Carlos‘ para 78.2. As linhas 12 à 14 exploram alguns atributos e métodos da classe Map. Para uma lista completa acesse a documentação em Map-class.

Expressões Condicionais: ? e ??

O Dart possui dois operadores que permitem avaliar expressões sem o uso da instrução if-else. Estas expressões são bem comuns em linguagens interpretadas como Script Shell, Python e Java Script, entre outras. Sua vantagem é a de serem sentenças compactas e muito práticas para uso em código.

A primeira forma possui a sintaxe a seguir:

condicional ? <expr1 se verdadeiro> : <expr2 se falso>

Se a condição for verdadeira a primeira expressão é avaliada, caso contrário a segunda expressão é avaliada.

void main() {
  const a = 1;
  const b = 2;

  (a > b) ? print('a ($a) é maior que b ($b)!') : print('b ($b) é maior que a ($a)!');
  // imprime: b (2) é maior que a (1)!
  
  int v = 63;
  
  String isV = (v % 2 == 0) ? 'é par' : 'é ímpar';
  
  print(isV); // é ímpar
}

Neste código a linha 5 emprega uma expressão condicional para imprimir ‘b (2) é maior que a (1)!‘ e na linha 10 isV é iniciada com a stringé ímpar‘ após a expressão ‘v % 2‘ retornar diferente de zero.

A segunda expressão condicional é usada para testar se uma variável é null. Sua sintaxe é apresentada a seguir:

<expr1 se não null> ?? <expr2 se null>

Por exemplo, imagine uma simples função que deve retornar o valor de uma variável name, caso esta não seja nula, e um valor padrão caso seja nula. Esta função pode ser declarada nas formas a seguir:

// Declaração convencional da função
String checkName(String? name) {
  if (name != null) {
    return name;
  } else {
  	return 'Erick';
  }
}

// Declaração da função com expressão condicional ??
String checkName(String? name) {
  return name ?? 'Erick';
}

// Declaração curta da função com expressão condicional ??
String checkName(String? name) => name ?? 'Erick';

A primeira forma é a convencional, linhas 2 à 8, usando uma sentença if-else para determinar o retorno da função. A segunda forma, linhas 11 à 13, emprega uma expressão condicional ?? para o retorno da função, e a terceira forma, linha 16, emprega uma declaração curta de função com a expressão condicional ??, muito empregada em códigos Dart. Funções são apresentadas ao final deste texto.

Outros Operadores

Os demais operadores serão deixados para serem apresentados adiante para não estender ainda mais sobre tópicos ainda não abordados.

Estruturas de Controle

O Dart possui 5 sentenças de controle de fluxo de execução, são elas:

  • Estruturas condicionais (if, else, else if);
  • Laços de repetição (for, while, do-while);
  • Switch case;
  • Estruturas de controle avançadas (break, continue)
  • função assert.

O assert já foi apresentado acima e não será estendido aqui. Ainda se pode adicionar a estas as keywords break e continue que são usados de forma convencionais nos laços for e while, além do emprego do break no switch-case para interromper a execução de um case.

Estruturas condicionais (if, else, else if)

O Dart suporta a sentença if-else com a sintaxe a seguir:

if (<condição_1>) {
  <comandos_1>;
} else if (<condição_2>) {
  <comandos_2>;
} else {
  <comandos_3>;
}

O código a seguir implementa um teste para o gerador de números aleatórios do Dart:

import 'dart:math';

void main() {
  Random rnd = Random();
  int countEven = 0;
  int countOdd = 0;
  const maxNumbers = 10000;

  for (int i = 0; i < maxNumbers; i++) {
    int n = rnd.nextInt(100);
    
    if (n % 2 == 0) {
      countEven++;
    } else {
      countOdd++;
    }
  }
  
  // if (n % 11 == 0) {
  //   print('$n é múltiplo de 11. Fim!');
  //   break;
  // }

  print('Gerado ${100*countEven/maxNumbers}% pares e ${100*countOdd/maxNumbers}% impares.');
  // retorna algo como: Gerado 50.1% pares e 49.9% impares.
}

Laço for

O Dart possui dois tipos de laços for. O primeiro convencional, estilo C é apresentado no código anterior, de sintaxe:

for (<declara variável>; <condição de saída do laço>; <incremento da variável>) {
  <comandos>;
}

Um break, como nos comentários das linhas 19 à 22, pode ser empregado para escapar do laço no caso de que alguma condição seja alcançada. Um continue faz com que o próximo laço da sentença for seja executado, ou seja, seguem os usos tradicionais tanto nos laços for como nos laços while a ser apresentado adiante.

O segundo tipo de laço é sobre os elementos de uma lista com a sintaxe apresentada a seguir:

for (<declara variável> in <lista de valores>) {
  <comandos_1>;
}

O código a seguir apresenta um exemplo do laço for sobre os elementos de uma lista:

void main() {
  const fruitList = ['apples', 'bananas', 'oranges'];

  for (String fruit in fruitList) {
    print('${fruit.toUpperCase()}');
  }
}

Método forEach() de uma Lista

O código acima pode ser refeito usando o método forEach() de uma lista como segue:

void main() {
  const fruitList = ['apples', 'bananas', 'oranges'];

  fruitList.forEach((fruit) {
    print('${fruit.toUpperCase()}');
  });
}

Diferentemente do Go, no Dart fazer a mesma coisa de diferentes formas parece ser algo desejado.

Laço While

O Dart também implementa os tradicionais laços while e do-while com sintaxes clássicas. Sintaxe do while:

while (<condição>) {
  <comandos>;
}

Sintaxe do do-while:

do {
  <comandos>;
} while (<condição>) ;

A diferença entre estes dois laços está no teste da condição de permanência no laço. No while o teste ocorre no início do laço, enquanto que no do-while ocorre ao final do laço.

O código a seguir reimplementa o código ‘even_odd.dart‘, apresentado acima, com o emprego do laço do-while:

import 'dart:math';

void main() {
  Random rnd = Random();
  int countEven = 0;
  int countOdd = 0;
  const maxNumbers = 10000;

  do {
    int n = rnd.nextInt(100);

    if (n % 2 == 0) {
      countEven++;
    } else {
      countOdd++;
    }
  } while (countEven + countOdd < maxNumbers);

  print('Gerado ${100 * countEven / maxNumbers}% pares e ${100 * countOdd / maxNumbers}% impares.');
  // possível retorno: Gerado 49.5% pares e 50.5% impares.
}

Sentença switch-case

A sentença switch-case do Dart é bastante limitada e trabalha apenas com a aplicação do operador de igualdade (==) aos valores no case, e cada case pode possuir apenas um valor constante para comparação. Bom, isto deve deixar o switch-case bem rápido mas também nada flexível.

Sua sintaxe segue abaixo:

switch (<variável>) {
  case <match1>:
    <comandos_1>;
    break;
  case <match2>:
    <comandos_2>;
    break;
  case <match_3>:
    <comandos_3>;
    break;
  default:
    <comandos_d>;
}

Os breaks são opcionais na sentença, no entanto, a sua ausência fará com que as demais cases sejam testados na sequência, o que geralmente não é desejado em um switch-case.

O código a seguir reimplementa o contador de pares e ímpares com um switch-case:

import 'dart:math';

void main() {
  Random rnd = Random();
  int countEven = 0;
  int countOdd = 0;
  const maxNumbers = 1000;

  do {
    int n = rnd.nextInt(100);

    switch (n % 2) {
      case 0:
        countEven++;
        break;
      default:
        countOdd++;
    }
  } while (countEven + countOdd < maxNumbers);
  
  print('Gerado ${100 * countEven / maxNumbers}% pares e ${100 * countOdd / maxNumbers}% impares.');
  // possível retorno: Gerado 49.5% pares e 50.5% impares.
}

Funções

No Dart funções são instâncias da classe Function com os atributos hashCode e runtimeType, e os métodos noSuchMethod (invocado quando uma função ou atributo não existente é invocado) e toString(). Uma função é declarada com a sintaxe abaixo:

<type> nomeDaFunção(<type> arg1, <type> arg2, ...) {
  <comandos>;
  [return var1];
}

O tipo pode ser void, se a função não gerar retorno, ou qualquer outro tipo.

Até o momento a função mais empregada nos códigos anteriores foi a função main(), sem argumentos. No entanto, esta função pode receber argumentos. O código a seguir implementa uma função main que pode receber indefinidos argumentos de entrada:

/* 
Execute num terminal

$ dart args.dart 1 alves true 12.5

*/
void main(List<String> args) {
  print('args length: ${args.length}');
  int i = 0;

  print(args);
  for (String arg in args) {
    print('$i: $arg');
    i++;
  }
}

Executado num terminal, conforme instruções na linha de comentário 4, todos os argumentos serão tratados como strings, gerando uma saída como a mostrada a seguir:

Bash

alves@arabel:json1$ dart args.dart 1 alves true 12.5
args length: 4
[1, alves, true, 12.5]
0: 1
1: alves
2: true
3: 12.5

O próximo código cria uma função title que retorna os nomes com a primeira letra de cada palavra em maiúscula, e as demais em minúsculas.

void main() {
  String name = 'alBERT einSTeiN';

  print(title(name));  // imprime "Albert Einstein"
}

String title(String name) {
  String out = '';
  for (String n in name.split(' ')) {
    out = '${out} ${n[0].toUpperCase()}${n.substring(1,).toLowerCase()}';
  }
  
  return out.trim();
}

O próximo código implementa um comando de terminal para calcular as raízes de um polinômio de segundo grau usando a fórmula de Bhaskara:

import 'dart:math';

/*
Com a entrada 
$ dart seggrau.dart 2 5 -3
Raízes reais:
x1 = 0.5
x2 = -3.0

$ dart seggrau.dart 2 -3 5
Raízes complexas:
z1 = 0.75 + i1.3919410907075054
z2 = 0.75 - i1.3919410907075054
*/

void main(List<String> args) {
  if (args.length == 3) {
    double a = double.tryParse(args[0]) ?? 0.0;
    double b = double.tryParse(args[1]) ?? 0.0;
    double c = double.tryParse(args[2]) ?? 0.0;

    raizes(a, b, c);
  } else {
    print('É necessário que sejam passados três argumentos:');
    print('a x² + b x + c = 0');
    print('\nseggrau a b c');    
  }
}

void raizes (double a, double b, double c) {
  double delta = pow(b,2)-4*a*c;

  if (delta >= 0) {
    double x1 = (-b + sqrt(delta))/(2*a);
    double x2 = (-b - sqrt(delta))/(2*a);

    print('Raízes reais:');
    print('x1 = $x1');
    print('x2 = $x2');
  } else {
    double real = -b/(2*a);
    double imag = sqrt(-delta)/(2*a);

    print('Raízes complexas:');
    print('z1 = $real + i$imag');
    print('z2 = $real - i$imag');
  }
}

Funções Compactas

O Dart também permite criar funções em uma forma compacta com a sintaxe:

<type> nomeFuncao(<type> arg1, ...) => comando;

O código a seguir implementa duas funções compactas para fazer o upperCase e title numa string passada.

void main() {
  String name = 'einstein';

  print(upperCase(name)); // imprime: EINSTEIN
  
  print(title(name));     // imprime: Einstein
  
}

String upperCase(String n) => n.toUpperCase();

String title(String n) => n[0].toUpperCase() + n.substring(1,).toLowerCase();

O upperCase é uma redundância, já que este apenas invoca um método de string.

Funções anônimas

Funções anônimas são funções, como diz o nome, não nomeadas. Geralmente são criadas para passar como argumentos para outras funções, métodos ou construtores, definidas diretamente sobre o código de passagem. Sua sintaxe é a mesma da função, mas sem o nome da função, ou seja:

<type> (<type> arg1, <type> arg2, ...) {
  <comandos>;
  [return var1];
}

Uma função anônima foi usada no código forEach.dart, acima, para implementar uma chamada ao método forEach() numa lista de strings. Este código é replicado abaixo destacando a função anônima para um melhor destaque.

void main() {
  const fruitList = ['apples', 'bananas', 'oranges'];

  fruitList.forEach(
    (fruit) {
      print(fruit.toUpperCase());
    }
  );
  
  // fruitList.forEach((fruit) {print(fruit.toUpperCase());});
  // fruitList.forEach((fruit) => print(fruit.toUpperCase()));
}

A função anônima é apenas as linhas 5 à 7. Não há a necessidade de expandir a função anônima como acima e, em geral, seu código é curto e geralmente aparece em poucas linhas. Em caso de funções mais longas é aconselhado o uso de funções externas para uma melhor legibilidade do código. Como exemplo as linhas 10 e 11, comentadas, apresentam o mesmo código em um linha.

O código a seguir implementa um title em uma nome completo e usa uma função anônima. Tente identificá-la.

void main() {
  var name = 'alberto santos dumont';
  print(titleAll(name));
}

String title(String n) => n[0].toUpperCase() + n.substring(1,).toLowerCase();

String titleAll(String name) {
  var newList = List.from(name.split(' ').map((item) => title(item)));
  
  return newList.join(' ');
}

A função anônima é usada na linha 9 para gerar o iterável pelo método map da lista criada pelo construtor List.from. Observe que esta está escrita no modo compacto e por isto não usa um return para retornar a string formatada. Problema em particular a função anônima é redundante, visto que o método map necessita apenas de receber a função title como segue:

void main() {
  var name = 'alberto santos dumont';
  print(titleAll(name));
}

String title(String n) => n[0].toUpperCase() + n.substring(1,).toLowerCase();

String titleAll(String name) {
  var newList = List.from(name.split(' ').map(title));
  
  return newList.join(' ');
}

ou compactando um pouco mais:

void main() {
  var name = 'alberto santos dumont';
  print(titleAll(name));
}

String title(String n) => n[0].toUpperCase() + n.substring(1,).toLowerCase();

String titleAll(String name) => List.from(name.split(' ').map(title)).join(' ');

Neste aspecto o Dart me lembra um pouco a linguagem C, onde o código pode ser compactado até ficar pouco legível.

void main() {
  var name = 'alberto santos dumont';
  print(titleAll(name));
}

String titleAll(String name) => List.from(name.split(' ').map((String n) => n[0].toUpperCase() + n.substring(1,).toLowerCase())).join(' ');

Parâmetros de Funções

Por padrão os argumentos de funções no Dart são posicionais e obrigatórios. No entanto é possível trabalhar com parâmetros não posicionais, não obrigatórios e nomeados, como em outras linguagens.

Argumentos Posicionais e Obrigatórios

Por exemplo, tome novamente o código para extrair as raízes de uma equação de segundo grau com algum simplificação:

import 'dart:math';

void main() {
  raizes(2, 3, -5);
  // raizes(2, 3);
  // raizes(2);
}

void raizes(double a, double b, double c) {
  double delta = pow(b, 2) - 4 * a * c;
  printEq(a, b, c);

  if (delta >= 0) {
    double x1 = (-b + sqrt(delta)) / (2 * a);
    double x2 = (-b - sqrt(delta)) / (2 * a);
    print('x1 = $x1');
    print('x2 = $x2\n');
  } else {
    print('Raízes imaginárias\n');
  }
}

void printEq(double a, double b, double c) {
  String out = '$a x²';
  if (b != 0) {
    out = (b < 0) ? '$out - ${-b}' : '$out + $b';
  }

  if (c != 0) {
    out = (c < 0) ? '$out - ${-c}' : '$out + $c';
  }
  print('$out = 0');
}

A função printEq() apenas imprime a equação resolvida para verificação e será ignorada nesta análise. A execução deste código vai gerar a saída:

2 x² + 3 - 5 = 0
x1 = 1
x2 = -2.5

Da forma como a função raizes() está definida é necessário que todas os três parâmetros de entrada da função sejam passados para que o código funciona. A entrada dos parâmetros para uma equação “2 x² + 3 x = 0” deve ser feita passando um zero para a o parâmetro b, na forma: raizes(2, 3, 0).

Argumentos Posicionais e Opcionais

A questão é como deixar com que os parâmetros b e c sejam posicionais e opcionais? Uma forma de fazer isto no Dart é declarar estes parâmetros entre colchetes como segue:

import 'dart:math';

void main() {
  raizes(2, 3, -5);
  raizes(2, 3);
  raizes(2);
}

void raizes(double a, [double b = 0, double c = 0]) {
  double delta = pow(b, 2) - 4 * a * c;
  printEq(a, b, c);

  if (delta >= 0) {
    double x1 = (-b + sqrt(delta)) / (2 * a);
    double x2 = (-b - sqrt(delta)) / (2 * a);
    print('x1 = $x1');
    print('x2 = $x2\n');
  } else {
    print('Raízes imaginárias\n');
  }
}

Como os parâmetros b e c agora são opcionais é necessário que um valor padrão seja declarado para elas. Desta fora as chamadas nas linhas 5 e 6, à função raizes() se tornam possíveis e os parâmetros b e c assumirão o valor 0 caso sejam omitidas.

Outra forma de declarar b e c na função raizes() seria permitir que elas assumam o valor null adicionando um ? após o double. O problema é que estes parâmetros devem assumir algum valor caso não sejam passadas para que as expressões matemáticas, nas linhas 10, 14 e 15, possam ser executadas. O código a seguir reimplementa a solução acima com parâmetros b e c nullables:

import 'dart:math';

void main() {
  raizes(2, 3, -5);
  raizes(2, 3);
  raizes(2);
}

void raizes(double a, [double? b, double? c]) {
  b = b ?? 0;
  c = c ?? 0;
  double delta = pow(b, 2) - 4 * a * c;
  printEq(a, b, c);

  if (delta >= 0) {
    double x1 = (-b + sqrt(delta)) / (2 * a);
    double x2 = (-b - sqrt(delta)) / (2 * a);
    print('x1 = $x1');
    print('x2 = $x2\n');
  } else {
    print('Raízes imaginárias\n');
  }
}

Para este problema em particular, fazer b e c variáveis nullables não me parece a melhor opção, no entanto, a solução funciona. Neste caso houve a necessidade da adição das linhas 10 e 11 para iniciar um valor no-nullable para as variáveis.

Seja pelo código seggrau1.dart ou seggrau2.dart a saída agora será:

2 x² + 3 - 5 = 0
x1 = 1
x2 = -2.5

2 x² + 3 = 0
x1 = 0
x2 = -1.5

2 x² = 0
x1 = 0
x2 = -0.0

Um problema de parâmetros posicionais é a necessidade de passar todos os parâmetros anteriores para pode alterar o valor do último. Imagina que tenhamos de resolver as raízes de:

2 x² - 5 = 0

ou seja, sem o parâmetro b. Na forma como definimos a função raizes() é necessário que se passe um zero para o b para poder passar o -5 para o c, ou seja, uma chamada como raizes(2, 0, -5). Para uma função com poucos parâmetros isto não é um problema e se pode chamar com um zero a mais sem grandes transtornos. No entanto, para funções mais complexas isto pode ficar bem mais difícil, obrigando o programador a entrar com uma grande quantidade de parâmetros nulos, zeros ou outro valor padrão, até que se consiga alcançar o parâmetro que de fato se deseja alterar.

Argumentos Nomeados, Opcionais e Não Posicionais

O que se busca aqui é fazer com que os parâmetros da função sejam nomeados, opcionais e não posicionais. A forma de fazer isto no Dart é declarando os parâmetros b e c entre parênteses, ao invés de colchetes. Para isto troque o colchetes por parênteses e faça mais algumas mudanças na chamada da função como abaixo:

import 'dart:math';

void main() {
  raizes(2, c:-5);
  raizes(2, b:3);
  raizes(2);
}

void raizes(double a, {double b = 0, double c = 0}) {
  double delta = pow(b, 2) - 4 * a * c;
  printEq(a, b, c);

  if (delta >= 0) {
    double x1 = (-b + sqrt(delta)) / (2 * a);
    double x2 = (-b - sqrt(delta)) / (2 * a);
    print('x1 = $x1');
    print('x2 = $x2\n');
  } else {
    print('Raízes imaginárias\n');
  }
}

Agora os parâmetros b e c agora são nomeáveis e por isto tem de se passar o nome do parâmetro seguido de dois pontos e o seu valor. Aqui optei por implementar apenas o código com as variáveis no-nullable.

Saído do campo da matemática, o próximo código cria um diálogo simples para terminal, onde uma mensagem é apresentada em uma caixa com linhas simples, duplas, centrada, à direita ou à esquerda

O código a seguir implementa este dialogo simples com parâmetros nomeados, opcionais e não posicionais:

import 'dart:math';

void main(List<String> args) {
  dialog('Olá mundo!', doubleLine: true);
  dialog('Olá mundo!', alignment: 'left');
  dialog('Olá mundo!', alignment: 'r', doubleLine: true);
}

void dialog(String msg, {String alignment = 'c', bool doubleLine = false}) {
  int lenAdj = (msg.length % 2); // 0 to even and 1 to odd msg length
  int boxSize = msg.length + 4 + lenAdj;
  int lineLength = 80;
  late int margin;
  int spaceLeft = (boxSize - msg.length) ~/ 2;
  int spaceRight = spaceLeft + lenAdj;

  switch (alignment) {
    case 'r':
    case 'right':
      margin = lineLength - boxSize;
      break;
    case 'l':
    case 'left':
      margin = 0;
      break;
    case 'c':
    case 'center':
      margin = (lineLength - boxSize) ~/ 2;
      break;
    default:
      print('Unsupported alignment option: "$alignment"');
      return;
  }

  if (!doubleLine) {
    print('${' '*margin}┌${'─'*boxSize}┐');
    print('${' '*margin}│${' '*spaceLeft}$msg${' '*spaceRight}│');
    print('${' '*margin}└${'─'*boxSize}┘');
  } else {
    print('${' '*margin}╔${'═'*boxSize}╗');
    print('${' '*margin}║${' '*spaceLeft}$msg${' '*spaceRight}║');
    print('${' '*margin}╚${'═'*boxSize}╝');
  }
}

Este código ainda deve implementar tamanho máximo para a mensagem e caixa de diálogo, quebra de linhas para as mensagens, adição de botões, … Tudo isto deixaria a função complexa e com muitos parâmetros de entrada, o que justificaria o uso de parâmetros nomeados, opcionais e não posicionais.

Considerações Finais

Este artigo é uma revisão pessoal da linguagem Dart passando por pontos essenciais mas sem se aprofundar nos diferentes tópicos. Devo ainda escrever mais um ou dois artigos no estilo para fechar a base de conhecimento do Dart, mas mantendo a ideia de ser apenas uma texto sucinto e sem grandes pretensões.

Boa parte deste conteúdo é retirado da documentação do Dart e aconselho consultá-la para maior precisão.

Este post tem um comentário

Deixe um comentário

Esse site utiliza o Akismet para reduzir spam. Aprenda como seus dados de comentários são processados.