Executando verificação de segurança...
1

Meu aprendizado em Dart! Introdução: parte 4!

Antes de começar a ler…

A ideia inicial era criar um artigo/tutorial, que contivesse os conhecimentos basico de Dart, e também algumas aplicações no final, mas, acabei percebendo que começou a ficar muito extenso, e por esse motivo decidi dividir em partes, essa primeira em até cinco partes ao longo da semana, essa sendo a quarta parte. Originalmente com mais de 80000(oitenta mil) caracteres para esse texto...

E como eu sempre falo, leia a documentação oficial, se for possível para você!

Até o atual momento da publicação deste texto, o TabNews, não formata o código em Dart corretamente, peço de antemão desculpas por qualquer inconveniência causada pela formatação incorreta do código.

Requisitos

Ter lido as primeiras partes do artigo/tutorial:

Meu aprendizado em Dart! Introdução: parte 1!

Meu aprendizado em Dart! Introdução: parte 2!

Meu aprendizado em Dart! Introdução: parte 3!


Overriding membros

As subclasses podem substituir métodos de instância (incluindo operadores), getters e setters. Você pode usar a anotação @override para indicar que está substituindo intencionalmente um membro:

class Television {
  // ···
  set contrast(int value) {...}
}
 
class SmartTelevision extends Television {
  @override
  set contrast(num value) {...}
  // ···
}

Tipos enumerados

Tipos enumerados, geralmente chamados de enumerações ou enums, são um tipo especial de classe usado para representar um número fixo de valores constantes.

Declarando enums simples

Para declarar um tipo enumerado simples, use a palavra-chave enum e liste os valores que deseja enumerar:

enum Color { red, green, blue }

Declarando enums aprimorados

O Dart também permite declarações de enum para declarar classes com campos, métodos e construtores const que são limitados a um número fixo de instâncias constantes conhecidas.

enum Vehicle implements Comparable<Vehicle> {
  car(tires: 4, passengers: 5, carbonPerKilometer: 400),
  bus(tires: 6, passengers: 50, carbonPerKilometer: 800),
  bicycle(tires: 2, passengers: 1, carbonPerKilometer: 0);
 
  const Vehicle({
    required this.tires,
    required this.passengers,
    required this.carbonPerKilometer,
  });
 
  final int tires;
  final int passengers;
  final int carbonPerKilometer;
 
  int get carbonFootprint => (carbonPerKilometer / passengers).round();
 
  @override
  int compareTo(Vehicle other) => carbonFootprint - other.carbonFootprint;
}

Usando enums

Acesse os valores enumerados como qualquer outra variável estática:

final favoriteColor = Color.blue;
if (favoriteColor == Color.blue) {
  print('Your favorite color is blue!');
}

Adicionando recursos a uma classe: mixins

Mixins são uma forma de reutilizar o código de uma classe em várias hierarquias de classe.

Para usar um mixin, use a palavra-chave with seguida por um ou mais nomes de mixin. O exemplo a seguir mostra duas classes que usam mixins:

class Musician extends Performer with Musical {
  // ···
}
 
class Maestro extends Person with Musical, Aggressive, Demented {
  Maestro(String maestroName) {
    name = maestroName;
    canConduct = true;
  }
}

Para implementar um mixin, crie uma classe que estenda Object e não declare nenhum construtor. A menos que você queira que seu mixin seja utilizável como uma classe regular, use a palavra-chave mixin em vez de class. Por exemplo:

mixin Musical {
  bool canPlayPiano = false;
  bool canCompose = false;
  bool canConduct = false;
 
  void entertainMe() {
    if (canPlayPiano) {
      print('Playing piano');
    } else if (canConduct) {
      print('Waving hands');
    } else {
      print('Humming to self');
    }
  }
}

Algumas vezes você pode querer restringir os tipos que podem usar um mixin. Por exemplo, o mixin pode depender da capacidade de invocar um método que o mixin não define. Como mostra o exemplo a seguir, você pode restringir o uso de um mixin usando a palavra-chave on para especificar a superclasse necessária:

class Musician {
  // ...
}
mixin MusicalPerformer on Musician {
  // ...
}
class SingerDancer extends Musician with MusicalPerformer {
  // …
}

No código anterior, apenas as classes que estendem ou implementam a classe Musician podem usar o mixin MusicalPerformer. Como o SingerDancer estende o Musician, o SingerDancer pode mixar no MusicalPerformer.

Variáveis ​​e métodos de classe

Use a palavra-chave static para implementar variáveis ​​e métodos em toda a classe.

Variáveis ​​estáticas

Variáveis ​​estáticas (variáveis ​​de classe) são úteis para estado e constantes de toda a classe:

class Queue {
  static const initialCapacity = 16;
  // ···
}
 
void main() {
  assert(Queue.initialCapacity == 16);
}

As variáveis ​​estáticas não são inicializadas até serem usadas.

Métodos estáticos

Os métodos estáticos (métodos de classe) não operam em uma instância e, portanto, não têm acesso a ela. Eles, no entanto, têm acesso a variáveis ​​estáticas. Como mostra o exemplo a seguir, você invoca métodos estáticos diretamente em uma classe:

import 'dart:math';
 
class Point {
  double x, y;
  Point(this.x, this.y);
 
  static double distanceBetween(Point a, Point b) {
    var dx = a.x - b.x;
    var dy = a.y - b.y;
    return sqrt(dx * dx + dy * dy);
  }
}
 
void main() {
  var a = Point(2, 2);
  var b = Point(4, 4);
  var distance = Point.distanceBetween(a, b);
  assert(2.8 < distance && distance < 2.9);
  print(distance);
}

Observação: Considere o uso de funções de nível superior, em vez de métodos estáticos, para utilitários e funcionalidades comuns ou amplamente usados.

Você pode usar métodos estáticos como constantes de tempo de compilação. Por exemplo, você pode passar um método estático como parâmetro para um construtor constante.

Generics

Se você olhar a documentação da API para o tipo de array básico, List, verá que o tipo é, na verdade, List<E>. A notação <…> marca List como um tipo genérico (ou parametrizado) — um tipo que possui parâmetros formais de tipo. Por convenção, a maioria das variáveis ​​de tipo tem nomes de uma única letra, como E, T, S, K e V.

Por que usar genéricos?

Geralmente, os genéricos são necessários para segurança de tipo, mas eles têm mais benefícios do que apenas permitir que seu código seja executado:

  • A especificação adequada de tipos genéricos resulta em um código gerado melhor.
  • Você pode usar genéricos para reduzir a duplicação de código.

Se você pretende que uma lista contenha apenas strings, você pode declará-la como List<String> (leia como lista de strings). Dessa forma, você, seus colegas programadores e suas ferramentas podem detectar que atribuir um não-string à lista provavelmente é um erro. Aqui está um exemplo:

var names = <String>[];
names.addAll(['Seth', 'Kathy', 'Lars']);
names.add(42); // Error

Outra razão para usar genéricos é reduzir a duplicação de código. Os genéricos permitem que você compartilhe uma única interface e implementação entre muitos tipos, enquanto ainda aproveita a análise estática. Por exemplo, digamos que você crie uma interface para armazenar em cache um objeto:

abstract class ObjectCache {
  Object getByKey(String key);
  void setByKey(String key, Object value);
}

Você descobre que deseja uma versão específica de string dessa interface, então cria outra interface:

abstract class StringCache {
  String getByKey(String key);
  void setByKey(String key, String value);
}

Mais tarde, você decide que quer uma versão específica de número desta interface... Você entendeu.

abstract class Cache<T> {
  T getByKey(String key);
  void setByKey(String key, T value);
}

Neste código, T é o tipo substituto. É um espaço reservado que você pode imaginar como um tipo que um desenvolvedor definirá posteriormente.

Usando literals de coleção

Os literais de lista, conjunto e mapa podem ser parametrizados. Os literais parametrizados são exatamente como os literais que você já viu, exceto que você adiciona <type> (para listas e conjuntos) ou <keyType, valueType> (para mapas) antes do colchete de abertura. Aqui está um exemplo de uso de literais digitados:

var names = <String>['Seth', 'Kathy', 'Lars'];
var uniqueNames = <String>{'Seth', 'Kathy', 'Lars'};
var pages = <String, String>{
  'index.html': 'Homepage',
  'robots.txt': 'Hints for web robots',
  'humans.txt': 'We are people, not machines'
};

Suporte para assincronia

As bibliotecas Dart estão repletas de funções que retornam objetos Future ou Stream. Essas funções são assíncronas: Elas retornam após a configuração de uma operação possivelmente demorada (como I/O), sem esperar que a operação seja concluída.

As palavras-chave async e await oferecem suporte à programação assíncrona, permitindo que você escreva um código assíncrono semelhante ao código síncrono.

Lidando com Futures

O código que usa async e await é assíncrono, mas se parece muito com o código síncrono. Por exemplo, aqui está um código que usa await para aguardar o resultado de uma função assíncrona:

await lookUpVersion();

Para usar await, o código deve estar em uma função assíncrona — uma função marcada como async:

Future<void> checkVersion() async {
  var version = await lookUpVersion();
  // Do something with version
}

Observação: Embora uma função assíncrona possa executar operações demoradas, ela não espera por essas operações. Em vez disso, a função assíncrona é executada apenas até encontrar sua primeira expressão de espera. Em seguida, ele retorna um objeto Future, retomando a execução somente após a conclusão da expressão await.

Use try, catch e, finally, para lidar com erros e limpeza no código que usa await:

try {
  version = await lookUpVersion();
} catch (e) {
  // React to inability to look up the version
}

Você pode usar await várias vezes em uma função assíncrona. Por exemplo, o seguinte código espera três vezes pelos resultados das funções:

var entrypoint = await findEntryPoint();
var exitCode = await runExecutable(entrypoint, args);
await flushThenExit(exitCode);

Na expressão await, o valor da expressão é geralmente um Future; se não for, o valor é automaticamente envolvido em um Future. Este objeto Future indica uma promessa que retorna um objeto. O valor da expressão await é aquele objeto retornado. A expressão await faz a execução pausar até que o objeto esteja disponível.

Se você receber um erro de tempo de compilação ao usar await, certifique-se de que await esteja em uma função assíncrona. Por exemplo, para usar await na função main() do seu aplicativo, o corpo de main() deve ser marcado como assíncrono:

void main() async {
  checkVersion();
  print('In main: version is ${await lookUpVersion()}');
}

Declarando funções assíncronas

Uma função assíncrona é uma função cujo corpo é marcado com o modificador async.

Adicionar a palavra-chave async a uma função faz com que ela retorne um Future. Por exemplo, considere esta função síncrona, que retorna uma String:

String lookUpVersion() => '1.0.0';

Se você alterá-lo para uma função assíncrona - por exemplo, porque uma implementação futura será demorada - o valor retornado é um Future:

Future<String> lookUpVersion() async => '1.0.0';

Observe que o corpo da função não precisa usar a API Future. Dart cria o objeto Future, se necessário. Se sua função não retornar um valor útil, torne seu tipo de retorno Future.

Manipulando Streams

Nota: Antes de usar await for, certifique-se de que isso torna o código mais claro e que você realmente deseja aguardar todos os resultados do stream. Por exemplo, você normalmente não deve usar await for para ouvintes de eventos de interface do usuário, porque as estruturas de interface do usuário enviam fluxos infinitos de eventos.

Um loop for assíncrono tem o seguinte formato:

await for (varOrType identifier in expression) {
  // Executes each time the stream emits a value.
}

O valor da expressão deve ser do tipo Stream. A execução ocorre da seguinte forma:

  1. Aguarde até que o fluxo emita um valor.
  2. Execute o corpo do loop for, com a variável definida para o valor emitido.
  3. Repita 1 e 2 até que o fluxo seja fechado.

Para parar de ouvir o stream, você pode usar uma instrução break ou return, que sai do loop for e cancela a assinatura do stream.

Se você receber um erro de tempo de compilação ao implementar um loop for assíncrono, verifique se o await for está em uma função assíncrona. Por exemplo, para usar um loop for assíncrono na função main() do seu aplicativo, o corpo de main() deve ser marcado como async:

void main() async {
  // ...
  await for (final request in requestServer) {
    handleRequest(request);
  }
  // …
}

Geradores

Quando você precisar produzir lentamente uma sequência de valores, considere o uso de uma função geradora. O Dart possui suporte integrado para dois tipos de funções do gerador:

  • Gerador síncrono: Retorna um objeto iterável.
  • Gerador assíncrono: Retorna um objeto Stream.

Para implementar uma função geradora síncrona, marque o corpo da função como sync* e use instruções yield para entregar valores:

Iterable<int> naturalsTo(int n) sync* {
  int k = 0;
  while (k < n) yield k++;
}

Para implementar uma função geradora assíncrona, marque o corpo da função como async* e use instruções yield para entregar valores:

Stream<int> asynchronousNaturalsTo(int n) async* {
  int k = 0;
  while (k < n) yield k++;
}

Se seu gerador for recursivo, você pode melhorar seu desempenho usando yield*:

Iterable<int> naturalsDownFrom(int n) sync* {
  if (n > 0) {
    yield n;
    yield* naturalsDownFrom(n - 1);
  }
}

Callable classes

Para permitir que uma instância de sua classe Dart seja chamada como uma função, implemente o método call().

O método call() permite que qualquer classe que o defina emule uma função. Este método oferece suporte à mesma funcionalidade que as funções normais, como parâmetros e tipos de retorno.

No exemplo a seguir, a classe WannabeFunction define uma função call() que pega três strings e as concatena, separando cada uma com um espaço e anexando uma exclamação.

class WannabeFunction {
  String call(String a, String b, String c) => '$a $b $c!';
}
 
var wf = WannabeFunction();
var out = wf('Hi', 'there,', 'gang');
 
void main() => print(out);

Isolates

A maioria dos computadores, mesmo em plataformas móveis, possui CPUs multi-core. Para aproveitar todos esses núcleos, os desenvolvedores tradicionalmente usam threads de memória compartilhada em execução simultânea. No entanto, a simultaneidade de estado compartilhado é propensa a erros e pode levar a códigos complicados.

Em vez de threads, todo o código Dart é executado dentro de isolates. Cada isolate Dart usa um único thread de execução e não compartilha objetos mutáveis ​​com outros isolates. Girar vários isolates cria vários encadeamentos de execução. Isso permite multi-threading sem sua principal desvantagem, condições de corrida.

Typedefs

Um alias de tipo - geralmente chamado de typedef porque é declarado com a palavra-chave typedef - é uma maneira concisa de se referir a um tipo. Aqui está um exemplo de declaração e uso de um alias de tipo chamado IntList:

typedef IntList = List<int>;
IntList il = [1, 2, 3];

Um alias de tipo pode ter parâmetros de tipo:

typedef ListMapper<X> = Map<X, List<X>>;
Map<String, List<String>> m1 = {}; // Verbose.
ListMapper<String> m2 = {}; // Same thing but shorter and clearer.

Recomendamos o uso de tipos de função embutidos em vez de typedefs para funções, na maioria das situações. No entanto, typedefs de função ainda podem ser úteis:

typedef Compare<T> = int Function(T a, T b);
 
int sort(int a, int b) => a - b;
 
void main() {
  assert(sort is Compare<int>); // True!
}

Continua No Próximo Episódio...

Carregando publicação patrocinada...