Pular para o conteúdo principal

Effective Dart: Uso

Diretrizes para usar os recursos da linguagem na escrita de código sustentável.

Você pode usar estas diretrizes todos os dias no corpo do seu código Dart. Os usuários da sua biblioteca podem não perceber que você internalizou as ideias aqui, mas os mantenedores certamente vão perceber.

Bibliotecas

#

Estas diretrizes ajudam você a compor seu programa a partir de vários arquivos de forma consistente e sustentável. Para manter estas diretrizes breves, elas usam "import" para cobrir as diretivas import e export. As diretrizes se aplicam igualmente a ambas.

FAÇA usar strings em diretivas part of

#

ia-translate: true ---Regra do linter: use_string_in_part_of_directives

Muitos desenvolvedores Dart evitam usar part por completo. Eles acham mais fácil raciocinar sobre o código quando cada biblioteca é um único arquivo. Se você optar por usar part para separar parte de uma biblioteca em outro arquivo, o Dart exige que o outro arquivo indique, por sua vez, de qual biblioteca ele é parte.

O Dart permite que a diretiva part of use o nome de uma biblioteca. Nomear bibliotecas é um recurso legado que agora é desencorajado. Nomes de biblioteca podem introduzir ambiguidade ao determinar a qual biblioteca uma parte pertence.

A sintaxe preferida é usar uma string URI que aponte diretamente para o arquivo da biblioteca. Se você tem uma biblioteca, my_library.dart, que contém:

my_library.dart
dart
library my_library;
​
part 'some/other/file.dart';

Então o arquivo part deve usar a string URI do arquivo da biblioteca:

bom dart
part of '../../my_library.dart';

E não o nome da biblioteca:

ruim dart
part of my_library;

NÃO importe bibliotecas que estão dentro do diretório src de outro pacote

#

ia-translate: true ---Regra do linter: implementation_imports

O diretório src dentro de lib está especificado para conter bibliotecas privadas à implementação do próprio pacote. A forma como os mantenedores de pacotes versionam o pacote leva essa convenção em conta. Eles são livres para fazer mudanças amplas no código dentro de src sem que isso seja uma mudança incompatível para o pacote.

Isso significa que, se você importar a biblioteca privada de outro pacote, uma versão pontual menor, teoricamente não incompatível, desse pacote pode quebrar o seu código.

NÃO permita que um caminho de import alcance dentro ou fora de lib

#

ia-translate: true ---Regra do linter: avoid_relative_lib_imports

Um import package: permite que você acesse uma biblioteca dentro do diretório lib de um pacote sem se preocupar com onde o pacote está armazenado no seu computador. Para que isso funcione, você não pode ter imports que exijam que lib esteja em algum local do disco em relação a outros arquivos. Em outras palavras, um caminho de import relativo em um arquivo dentro de lib não pode sair e acessar um arquivo fora do diretório lib, e uma biblioteca fora de lib não pode usar um caminho relativo para alcançar o diretório lib. Fazer qualquer uma dessas coisas leva a erros confusos e programas quebrados.

Por exemplo, suponha que a estrutura de diretórios seja assim:

  • my_package/
    • lib/
      • api.dart
    • test/
      • api_test.dart

E suponha que api_test.dart importe api.dart de duas formas:

api_test.dart
ruim dart
import 'package:my_package/api.dart';
import '../lib/api.dart';

O Dart entende que são imports de duas bibliotecas completamente não relacionadas. Para evitar confundir o Dart e a você mesmo, siga estas duas regras:

  • Não use /lib/ em caminhos de import.
  • Não use ../ para sair do diretório lib.

Em vez disso, quando você precisar alcançar o diretório lib de um pacote (mesmo a partir do diretório test do mesmo pacote ou de qualquer outro diretório de nível superior), use um import package:.

api_test.dart
bom dart
import 'package:my_package/api.dart';

Um pacote nunca deve sair para fora do seu diretório lib e importar bibliotecas de outros lugares do pacote.

PREFIRA caminhos de import relativos

#

ia-translate: true ---Regra do linter: prefer_relative_imports

Sempre que a regra anterior não se aplicar, siga esta. Quando um import não atravessa lib, prefira usar imports relativos. Eles são mais curtos. Por exemplo, suponha que a estrutura de diretórios seja assim:

  • my_package/
    • lib/
      • src/
        • stuff.dart
        • utils.dart
      • api.dart
    • test/
      • api_test.dart
      • test_utils.dart

Veja como as várias bibliotecas devem importar umas às outras:

lib/api.dart
bom dart
import 'src/stuff.dart';
import 'src/utils.dart';
lib/src/utils.dart
bom dart
import '../api.dart';
import 'stuff.dart';
test/api_test.dart
bom dart
import 'package:my_package/api.dart'; // Don't reach into 'lib'.
​
import 'test_utils.dart'; // Relative within 'test' is fine.

Null

#

NÃO inicialize variáveis explicitamente para null

#

ia-translate: true ---Regra do linter: avoid_init_to_null

Se uma variável tem um tipo não nullable, o Dart reporta um erro de compilação se você tentar usá-la antes de ela ser definitivamente inicializada. Se a variável é nullable, ela é implicitamente inicializada com null para você. Não existe o conceito de "memória não inicializada" no Dart, e não há necessidade de inicializar explicitamente uma variável com null para estar "segura".

bom dart
Item? bestDeal(List<Item> cart) {
  Item? bestItem;
​
  for (final item in cart) {
    if (bestItem == null || item.price < bestItem.price) {
      bestItem = item;
    }
  }
​
  return bestItem;
}
ruim dart
Item? bestDeal(List<Item> cart) {
  Item? bestItem = null;
​
  for (final item in cart) {
    if (bestItem == null || item.price < bestItem.price) {
      bestItem = item;
    }
  }
​
  return bestItem;
}

NÃO use um valor padrão explícito de null

#

ia-translate: true ---Regra do linter: avoid_init_to_null

Se você torna opcional um parâmetro nullable, mas não lhe dá um valor padrão, a linguagem usa implicitamente null como padrão, então não há necessidade de escrevê-lo.

bom dart
void error([String? message]) {
  stderr.write(message ?? '\n');
}
ruim dart
void error([String? message = null]) {
  stderr.write(message ?? '\n');
}

NÃO use true ou false em operações de igualdade

#

Usar o operador de igualdade para avaliar uma expressão booleana não nullable contra um literal booleano é redundante. É sempre mais simples eliminar o operador de igualdade e usar o operador de negação unário ! se necessário:

bom dart
if (nonNullableBool) {
   ...
}
​
if (!nonNullableBool) {
   ...
}
ruim dart
if (nonNullableBool == true) {
   ...
}
​
if (nonNullableBool == false) {
   ...
}

Para avaliar uma expressão booleana que é nullable, você deve usar ?? ou uma verificação explícita != null.

bom dart
// If you want null to result in false:
if (nullableBool ?? false) {
   ...
}
​
// If you want null to result in false
// and you want the variable to type promote:
if (nullableBool != null && nullableBool) {
   ...
}
ruim dart
// Static error if null:
if (nullableBool) {
   ...
}
​
// If you want null to be false:
if (nullableBool == true) {
   ...
}

nullableBool == true é uma expressão viável, mas não deve ser usada por vários motivos:

  • Ela não indica que o código tem alguma relação com null.

  • Como não é evidentemente relacionada a null, ela pode ser facilmente confundida com o caso não nullable, em que o operador de igualdade é redundante e pode ser removido. Isso só é verdade quando a expressão booleana à esquerda não tem chance de produzir null, mas não quando pode produzir.

  • A lógica booleana é confusa. Se nullableBool é null, então nullableBool == true significa que a condição avalia para false.

O operador ?? deixa claro que algo relacionado a null está acontecendo, então não será confundido com uma operação redundante. A lógica também fica muito mais clara; o resultado de a expressão ser null é o mesmo que o literal booleano.

Usar um operador ciente de null, como ??, em uma variável dentro de uma condição não promove a variável para um tipo não nullable. Se você quer que a variável seja promovida dentro do corpo da instrução if, é melhor usar uma verificação explícita != null em vez de ??.

EVITE variáveis late se você precisar verificar se elas foram inicializadas

#

O Dart não oferece uma forma de saber se uma variável late foi inicializada ou recebeu um valor. Se você a acessa, ela executa imediatamente o inicializador (se houver um) ou lança uma exceção. Às vezes você tem algum estado inicializado de forma preguiçosa em que late pode ser uma boa escolha, mas também precisa ser capaz de saber se a inicialização já aconteceu.

Embora você pudesse detectar a inicialização armazenando o estado em uma variável late e tendo um campo booleano separado que controla se a variável foi definida, isso é redundante, porque o Dart internamente mantém o status de inicialização da variável late. Em vez disso, geralmente é mais claro tornar a variável não late e nullable. Então você pode ver se a variável foi inicializada verificando se ela é null.

É claro que, se null for um valor inicializado válido para a variável, então provavelmente faz sentido ter um campo booleano separado.

CONSIDERE promoção de tipo ou padrões de verificação de null para usar tipos nullable

#

Verificar que uma variável nullable não é igual a null promove a variável para um tipo não nullable. Isso permite acessar membros da variável e passá-la para funções que esperam um tipo não nullable.

A promoção de tipo só é suportada, porém, para variáveis locais, parâmetros e campos final privados. Valores sujeitos a manipulação não podem ter o tipo promovido.

Declarar membros privados e final, como geralmente recomendamos, muitas vezes basta para contornar essas limitações. Mas nem sempre essa é uma opção.

Um padrão para contornar as limitações de promoção de tipo é usar um padrão de verificação de null. Ele ao mesmo tempo confirma que o valor do membro não é null e vincula esse valor a uma nova variável não nullable do mesmo tipo base.

bom dart
class UploadException {
  final Response? response;
​
  UploadException([this.response]);
​
  @override
  String toString() {
    if (this.response case var response?) {
      return 'Could not complete upload to ${response.url} '
          '(error code ${response.errorCode}): ${response.reason}.';
    }
    return 'Could not upload (no response).';
  }
}

Outra solução alternativa é atribuir o valor do campo a uma variável local. Verificações de null nessa variável vão promover, então você pode tratá-la com segurança como não nullable.

bom dart
class UploadException {
  final Response? response;
​
  UploadException([this.response]);
​
  @override
  String toString() {
    final response = this.response;
    if (response != null) {
      return 'Could not complete upload to ${response.url} '
          '(error code ${response.errorCode}): ${response.reason}.';
    }
    return 'Could not upload (no response).';
  }
}

Tenha cuidado ao usar uma variável local. Se você precisar gravar de volta no campo, certifique-se de não gravar na variável local. (Tornar a variável local final pode evitar esses erros.) Além disso, se o campo puder mudar enquanto a local ainda estiver no escopo, a local pode ter um valor desatualizado.

Às vezes, o melhor é simplesmente usar ! no campo. Em alguns casos, porém, usar uma variável local ou um padrão de verificação de null pode ser mais limpo e mais seguro do que usar ! toda vez que você precisar tratar o valor como não null:

ruim dart
class UploadException {
  final Response? response;
​
  UploadException([this.response]);
​
  @override
  String toString() {
    if (response != null) {
      return 'Could not complete upload to ${response!.url} '
          '(error code ${response!.errorCode}): ${response!.reason}.';
    }
​
    return 'Could not upload (no response).';
  }
}

Strings

#

Aqui estão algumas boas práticas a ter em mente ao compor strings em Dart.

FAÇA usar strings adjacentes para concatenar literais de string

#

ia-translate: true ---Regra do linter: prefer_adjacent_string_concatenation

Se você tem dois literais de string (não valores, mas a forma literal entre aspas), não precisa usar + para concatená-los. Assim como em C e C++, basta colocá-los lado a lado. Essa é uma boa forma de criar uma única string longa que não cabe em uma linha.

bom dart
raiseAlarm(
  'ERROR: Parts of the spaceship are on fire. Other '
  'parts are overrun by martians. Unclear which are which.',
);
ruim dart
raiseAlarm(
  'ERROR: Parts of the spaceship are on fire. Other ' +
      'parts are overrun by martians. Unclear which are which.',
);

PREFIRA usar interpolação para compor strings e valores

#

ia-translate: true ---Regra do linter: prefer_interpolation_to_compose_strings

Se você vem de outras linguagens, está acostumado a usar longas cadeias de + para montar uma string a partir de literais e outros valores. Isso funciona no Dart, mas quase sempre é mais limpo e curto usar interpolação:

bom dart
'Hello, $name! You are ${year - birth} years old.';
ruim dart
'Hello, ' + name + '! You are ' + (year - birth).toString() + ' y...';

Observe que esta diretriz se aplica à combinação de vários literais e valores. É aceitável usar .toString() ao converter apenas um único objeto em string.

EVITE usar chaves na interpolação quando não forem necessárias

#

ia-translate: true ---Regra do linter: unnecessary_brace_in_string_interps

Se você está interpolando um identificador simples que não é seguido imediatamente por mais texto alfanumérico, as {} devem ser omitidas.

bom dart
var greeting = 'Hi, $name! I love your ${decade}s costume.';
ruim dart
var greeting = 'Hi, ${name}! I love your ${decade}s costume.';

Coleções

#

De fábrica, o Dart oferece quatro tipos de coleção: listas, mapas, filas e conjuntos. As boas práticas a seguir se aplicam às coleções.

FAÇA usar literais de coleção sempre que possível

#

ia-translate: true ---Regra do linter: prefer_collection_literals

O Dart tem três tipos de coleção principais: List, Map e Set. As classes Map e Set têm construtores sem nome, como a maioria das classes. Mas como essas coleções são usadas com tanta frequência, o Dart tem uma sintaxe embutida mais agradável para criá-las:

bom dart
var points = <Point>[];
var addresses = <String, Address>{};
var counts = <int>{};
ruim dart
var addresses = Map<String, Address>();
var counts = Set<int>();

Observe que esta diretriz não se aplica aos construtores nomeados dessas classes. List.from(), Map.fromIterable() e outros têm seus usos. (A classe List também tem um construtor sem nome, mas ele é proibido no Dart com null safety.)

Literais de coleção são especialmente poderosos no Dart porque dão acesso ao operador spread para incluir o conteúdo de outras coleções, e if e for para executar controle de fluxo enquanto constrói o conteúdo:

bom dart
var arguments = [
  ...options,
  command,
  ...?modeFlags,
  for (var path in filePaths)
    if (path.endsWith('.dart')) path.replaceAll('.dart', '.js'),
];
ruim dart
var arguments = <String>[];
arguments.addAll(options);
arguments.add(command);
if (modeFlags != null) arguments.addAll(modeFlags);
arguments.addAll(
  filePaths
      .where((path) => path.endsWith('.dart'))
      .map((path) => path.replaceAll('.dart', '.js')),
);

NÃO use .length para verificar se uma coleção está vazia

#

ia-translate: true ---Regras do linter: prefer_is_empty, prefer_is_not_empty

O contrato de Iterable não exige que uma coleção conheça seu tamanho nem que consiga fornecê-lo em tempo constante. Chamar .length só para ver se a coleção contém algo pode ser dolorosamente lento.

Em vez disso, existem getters mais rápidos e mais legíveis: .isEmpty e .isNotEmpty. Use o que não exige negar o resultado.

bom dart
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
ruim dart
if (lunchBox.length == 0) return 'so hungry...';
if (!words.isEmpty) return words.join(' ');

EVITE usar Iterable.forEach() com um literal de função

#

ia-translate: true ---Regra do linter: avoid_function_literals_in_foreach_calls

As funções forEach() são muito usadas em JavaScript porque o loop embutido for-in não faz o que você normalmente quer. No Dart, se você quer percorrer uma sequência, a forma idiomática de fazer isso é usando um loop.

bom dart
for (final person in people) {
  ...
}
ruim dart
people.forEach((person) {
  ...
});

Observe que esta diretriz diz especificamente "literal de função". Se você quer invocar alguma função já existente em cada elemento, forEach() está ok.

bom dart
people.forEach(print);

Observe também que sempre é aceitável usar Map.forEach(). Mapas não são iteráveis, então esta diretriz não se aplica.

NÃO use List.from() a menos que você pretenda mudar o tipo do resultado

#

Dado um Iterable, há duas formas óbvias de produzir uma nova List que contém os mesmos elementos:

dart
var copy1 = iterable.toList();
var copy2 = List.from(iterable);

A diferença óbvia é que a primeira é mais curta. A diferença importante é que a primeira preserva o argumento de tipo do objeto original:

bom dart
// Creates a List<int>:
var iterable = [1, 2, 3];
​
// Prints "List<int>":
print(iterable.toList().runtimeType);
ruim dart
// Creates a List<int>:
var iterable = [1, 2, 3];
​
// Prints "List<dynamic>":
print(List.from(iterable).runtimeType);

Se você quer mudar o tipo, então chamar List.from() é útil:

bom dart
var numbers = [1, 2.3, 4]; // List<num>.
numbers.removeAt(1); // Now it only contains integers.
var ints = List<int>.from(numbers);

Mas se seu objetivo é apenas copiar o iterable e preservar seu tipo original, ou você não se importa com o tipo, então use toList().

FAÇA usar whereType() para filtrar uma coleção por tipo

#

ia-translate: true ---Regra do linter: prefer_iterable_whereType

Suponha que você tenha uma lista contendo uma mistura de objetos e queira obter apenas os inteiros dela. Você poderia usar where() assim:

ruim dart
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.where((e) => e is int);

Isso é verboso, mas, pior, retorna um iterable cujo tipo provavelmente não é o que você quer. No exemplo aqui, ele retorna um Iterable<Object>, embora você provavelmente queira um Iterable<int>, já que esse é o tipo para o qual você está filtrando.

Às vezes você vê código que "corrige" o erro acima adicionando cast():

ruim dart
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.where((e) => e is int).cast<int>();

Isso é verboso e cria dois wrappers, com duas camadas de indireção e verificação redundante em tempo de execução. Felizmente, a biblioteca core tem o método whereType() exatamente para este caso de uso:

bom dart
var objects = [1, 'a', 2, 'b', 3];
var ints = objects.whereType<int>();

Usar whereType() é conciso, produz um Iterable do tipo desejado e não tem níveis desnecessários de wrapping.

NÃO use cast() quando uma operação próxima resolver

#

Muitas vezes, quando você lida com um iterable ou stream, faz várias transformações nele. No final, você quer produzir um objeto com um certo argumento de tipo. Em vez de acrescentar uma chamada a cast(), veja se uma das transformações existentes pode mudar o tipo.

Se você já está chamando toList(), substitua essa chamada por uma chamada a List<T>.from(), em que T é o tipo da lista resultante que você quer.

bom dart
var stuff = <dynamic>[1, 2];
var ints = List<int>.from(stuff);
ruim dart
var stuff = <dynamic>[1, 2];
var ints = stuff.toList().cast<int>();

Se você está chamando map(), dê a ele um argumento de tipo explícito para que ele produza um iterable do tipo desejado. A inferência de tipos muitas vezes escolhe o tipo correto para você com base na função que você passa para map(), mas às vezes você precisa ser explícito.

bom dart
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map<double>((n) => n * 2);
ruim dart
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map((n) => n * 2).cast<double>();

EVITE usar cast()

#

Esta é a generalização mais branda da regra anterior. Às vezes não há nenhuma operação próxima que você possa usar para corrigir o tipo de algum objeto. Mesmo assim, quando possível, evite usar cast() para "mudar" o tipo de uma coleção.

Prefira qualquer uma destas opções:

  • Crie-a com o tipo certo. Altere o código onde a coleção é criada pela primeira vez para que ela tenha o tipo certo.

  • Converta os elementos no acesso. Se você vai percorrer a coleção imediatamente, converta cada elemento dentro da iteração.

  • Converta antecipadamente usando List.from(). Se você vai acessar a maioria dos elementos da coleção, e não precisa que o objeto seja sustentado pelo objeto original ativo, converta-o usando List.from().

    O método cast() retorna uma coleção preguiçosa que verifica o tipo do elemento em cada operação. Se você executa apenas algumas operações em poucos elementos, essa preguiça pode ser boa. Mas, em muitos casos, o custo da validação preguiçosa e do wrapping supera os benefícios.

Aqui está um exemplo de criá-la com o tipo certo:

bom dart
List<int> singletonList(int value) {
  var list = <int>[];
  list.add(value);
  return list;
}
ruim dart
List<int> singletonList(int value) {
  var list = []; // List<dynamic>.
  list.add(value);
  return list.cast<int>();
}

Aqui está convertendo cada elemento no acesso:

bom dart
void printEvens(List<Object> objects) {
  // We happen to know the list only contains ints.
  for (final n in objects) {
    if ((n as int).isEven) print(n);
  }
}
ruim dart
void printEvens(List<Object> objects) {
  // We happen to know the list only contains ints.
  for (final n in objects.cast<int>()) {
    if (n.isEven) print(n);
  }
}

Aqui está convertendo antecipadamente usando List.from():

bom dart
int median(List<Object> objects) {
  // We happen to know the list only contains ints.
  var ints = List<int>.from(objects);
  ints.sort();
  return ints[ints.length ~/ 2];
}
ruim dart
int median(List<Object> objects) {
  // We happen to know the list only contains ints.
  var ints = objects.cast<int>();
  ints.sort();
  return ints[ints.length ~/ 2];
}

Essas alternativas nem sempre funcionam, claro, e às vezes cast() é a resposta certa. Mas considere esse método um pouco arriscado e indesejável: ele pode ser lento e pode falhar em tempo de execução se você não tomar cuidado.

Funções

#

No Dart, até as funções são objetos. Aqui estão algumas boas práticas envolvendo funções.

FAÇA usar uma declaração de função para vincular uma função a um nome

#

ia-translate: true ---Regra do linter: prefer_function_declarations_over_variables

Linguagens modernas perceberam o quanto funções locais aninhadas e closures são úteis. É comum ter uma função definida dentro de outra. Em muitos casos, essa função é usada imediatamente como callback e não precisa de nome. Uma expressão de função é ótima para isso.

Mas, se você realmente precisar dar um nome a ela, use uma instrução de declaração de função em vez de vincular uma lambda a uma variável.

bom dart
void main() {
  void localFunction() {
    ...
  }
}
ruim dart
void main() {
  var localFunction = () {
    ...
  };
}

NÃO crie uma lambda quando um tear-off resolver

#

ia-translate: true ---Regra do linter: unnecessary_lambdas

Quando você se refere a uma função, método ou construtor nomeado sem parênteses, o Dart cria um tear-off. Este é um closure que recebe o mesmo parâmetros da função e invoca a função subjacente quando você a chama. Se o seu código precisa de um closure que invoque uma função nomeada com os mesmos parâmetros que o closure aceita, não envolva a chamada em uma lambda. Use um tear-off.

bom dart
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();
​
// Function:
charCodes.forEach(print);
​
// Method:
charCodes.forEach(buffer.write);
​
// Named constructor:
var strings = charCodes.map(String.fromCharCode);
​
// Unnamed constructor:
var buffers = charCodes.map(StringBuffer.new);
ruim dart
var charCodes = [68, 97, 114, 116];
var buffer = StringBuffer();
​
// Function:
charCodes.forEach((code) {
  print(code);
});
​
// Method:
charCodes.forEach((code) {
  buffer.write(code);
});
​
// Named constructor:
var strings = charCodes.map((code) => String.fromCharCode(code));
​
// Unnamed constructor:
var buffers = charCodes.map((code) => StringBuffer(code));

Variáveis

#

As boas práticas a seguir descrevem como usar bem as variáveis no Dart.

FAÇA seguir uma regra consistente para var e final em variáveis locais

#

A maioria das variáveis locais não deve ter anotações de tipo e deve ser declarada usando apenas var ou final. Há duas regras muito usadas para decidir quando usar um ou outro:

  • Use final para variáveis locais que não são reatribuídas e var para as que são.

  • Use var para todas as variáveis locais, mesmo as que não são reatribuídas. Nunca use final em locais. (Usar final em campos e variáveis de nível superior continua sendo recomendado, claro.)

Qualquer uma das regras é aceitável, mas escolha uma e aplique-a de forma consistente em todo o seu código. Assim, quando um leitor vir var, ele saberá se isso significa que a variável é atribuída mais adiante na função.

EVITE armazenar o que você pode calcular

#

Ao projetar uma classe, muitas vezes você quer expor várias visões do mesmo estado subjacente. Com frequência, você vê código que calcula todas essas visões no construtor e depois as armazena:

ruim dart
class Circle {
  double radius;
  double area;
  double circumference;
​
  Circle(double radius)
    : radius = radius,
      area = pi * radius * radius,
      circumference = pi * 2.0 * radius;
}

Esse código tem duas coisas erradas. Primeiro, provavelmente está desperdiçando memória. A área e a circunferência, estritamente falando, são caches. São cálculos armazenados que poderíamos recalcular a partir de outros dados que já temos. Eles trocam mais memória por menos uso de CPU. Sabemos que temos um problema de desempenho que justifique essa troca?

Pior, o código está errado. O problema com caches é a invalidação: como você sabe quando o cache está desatualizado e precisa ser recalculado? Aqui, nunca sabemos, mesmo que radius seja mutável. Você pode atribuir outro valor e area e circumference vão manter seus valores anteriores, agora incorretos.

Para tratar corretamente a invalidação do cache, precisaríamos fazer isto:

ruim dart
class Circle {
  double _radius;
  double get radius => _radius;
  set radius(double value) {
    _radius = value;
    _recalculate();
  }
​
  double _area = 0.0;
  double get area => _area;
​
  double _circumference = 0.0;
  double get circumference => _circumference;
​
  Circle(this._radius) {
    _recalculate();
  }
​
  void _recalculate() {
    _area = pi * _radius * _radius;
    _circumference = pi * 2.0 * _radius;
  }
}

É muito código para escrever, manter, depurar e ler. Em vez disso, sua primeira implementação deve ser:

bom dart
class Circle {
  double radius;
​
  Circle(this.radius);
​
  double get area => pi * radius * radius;
  double get circumference => pi * 2.0 * radius;
}

Esse código é mais curto, usa menos memória e é menos propenso a erros. Ele armazena a quantidade mínima de dados necessária para representar o círculo. Não há campos para ficar fora de sincronia, porque existe apenas uma única fonte de verdade.

Em alguns casos, você pode precisar armazenar em cache o resultado de um cálculo lento, mas faça isso somente depois de saber que tem um problema de desempenho, faça com cuidado e deixe um comentário explicando a otimização.

Membros

#

No Dart, objetos têm membros, que podem ser funções (métodos) ou dados (variáveis de instância). As boas práticas a seguir se aplicam aos membros de um objeto.

NÃO envolva um campo em um getter e setter sem necessidade

#

ia-translate: true ---Regra do linter: unnecessary_getters_setters

Em Java e C#, é comum esconder todos os campos atrás de getters e setters (ou propriedades, em C#), mesmo quando a implementação apenas repassa para o campo. Assim, se você precisar fazer mais trabalho nesses membros algum dia, pode fazê-lo sem precisar mexer nos pontos de chamada. Isso acontece porque chamar um método getter é diferente de acessar um campo em Java, e acessar uma propriedade não é compatível em nível binário com acessar um campo bruto em C#.

O Dart não tem essa limitação. Campos e getters/setters são completamente indistinguíveis. Você pode expor um campo em uma classe e, mais tarde, envolvê-lo em um getter e setter sem precisar tocar em nenhum código que usa esse campo.

bom dart
class Box {
  Object? contents;
}
ruim dart
class Box {
  Object? _contents;
  Object? get contents => _contents;
  set contents(Object? value) {
    _contents = value;
  }
}

PREFIRA usar um campo final para criar uma propriedade somente leitura

#

Se você tem um campo que código externo deve conseguir ver, mas não atribuir, uma solução simples que funciona em muitos casos é simplesmente marcá-lo como final.

bom dart
class Box {
  final contents = [];
}
ruim dart
class Box {
  Object? _contents;
  Object? get contents => _contents;
}

É claro que, se você precisar atribuir internamente ao campo fora do construtor, pode ser necessário usar o padrão "campo privado, getter público", mas não recorra a isso até precisar.

CONSIDERE usar => para membros simples

#

ia-translate: true ---Regra do linter: prefer_expression_function_bodies

Além de usar => para expressões de função, o Dart também permite definir membros com ele. Esse estilo combina bem com membros simples que apenas calculam e retornam um valor.

bom dart
double get area => (right - left) * (bottom - top);
​
String capitalize(String name) =>
    '${name[0].toUpperCase()}${name.substring(1)}';

Pessoas escrevendo código parecem adorar =>, mas é muito fácil abusar dele e acabar com código difícil de ler. Se a sua declaração tem mais do que algumas linhas ou contém expressões profundamente aninhadas (cascatas e operadores condicionais são causas comuns), faça um favor a você mesmo e a todos que vão ler seu código: use um corpo em bloco e algumas instruções.

bom dart
Treasure? openChest(Chest chest, Point where) {
  if (_opened.containsKey(chest)) return null;
​
  var treasure = Treasure(where);
  treasure.addAll(chest.contents);
  _opened[chest] = treasure;
  return treasure;
}
ruim dart
Treasure? openChest(Chest chest, Point where) => _opened.containsKey(chest)
    ? null
    : _opened[chest] = (Treasure(where)..addAll(chest.contents));

Você também pode usar => em membros que não retornam valor. Isso é idiomático quando um setter é pequeno e tem um getter correspondente que usa =>.

bom dart
num get x => center.x;
set x(num value) => center = Point(value, center.y);

NÃO use this. exceto para redirecionar a um construtor nomeado ou para evitar sombreamento

#

ia-translate: true ---Regra do linter: unnecessary_this

JavaScript exige um this. explícito para se referir a membros do objeto cujo método está sendo executado no momento, mas o Dart, assim como C++, Java e C#, não tem essa limitação.

Há apenas duas situações em que você precisa usar this.. Uma é quando uma variável local com o mesmo nome sombreia o membro que você quer acessar:

ruim dart
class Box {
  Object? value;
​
  void clear() {
    this.update(null);
  }
​
  void update(Object? value) {
    this.value = value;
  }
}
bom dart
class Box {
  Object? value;
​
  void clear() {
    update(null);
  }
​
  void update(Object? value) {
    this.value = value;
  }
}

A outra situação em que se usa this. é ao redirecionar para um construtor nomeado:

ruim dart
class ShadeOfGray {
  final int brightness;
​
  ShadeOfGray(int val) : brightness = val;
​
  ShadeOfGray.black() : this(0);
​
  // This won't parse or compile!
  // ShadeOfGray.alsoBlack() : black();
}
bom dart
class ShadeOfGray {
  final int brightness;
​
  ShadeOfGray(int val) : brightness = val;
​
  ShadeOfGray.black() : this(0);
​
  // But now it will!
  ShadeOfGray.alsoBlack() : this.black();
}

Observe que parâmetros de construtor nunca sombreiam campos nas listas de inicializadores do construtor:

bom dart
class Box extends BaseBox {
  Object? value;
​
  Box(Object? value) : value = value, super(value);
}

Isso parece surpreendente, mas funciona como você quer. Felizmente, código assim é relativamente raro graças aos parâmetros inicializadores e aos inicializadores de super.

FAÇA inicializar campos na sua declaração sempre que possível

#

Se um campo não depende de nenhum parâmetro de construtor, ele pode e deve ser inicializado na sua declaração. Isso exige menos código e evita duplicação quando a classe tem vários construtores.

ruim dart
class ProfileMark {
  final String name;
  final DateTime start;
​
  ProfileMark(this.name) : start = DateTime.now();
  ProfileMark.unnamed() : name = '', start = DateTime.now();
}
bom dart
class ProfileMark {
  final String name;
  final DateTime start = DateTime.now();
​
  ProfileMark(this.name);
  ProfileMark.unnamed() : name = '';
}

Alguns campos não podem ser inicializados na sua declaração porque precisam referenciar this, por exemplo, para usar outros campos ou chamar métodos. No entanto, se o campo for marcado como late, o inicializador pode acessar this.

É claro que, se um campo depende de parâmetros de construtor, ou é inicializado de forma diferente por construtores diferentes, então esta diretriz não se aplica.

Construtores

#

As boas práticas a seguir se aplicam à declaração de construtores de uma classe.

FAÇA usar parâmetros inicializadores sempre que possível

#

ia-translate: true ---Regra do linter: prefer_initializing_formals

Muitos campos são inicializados diretamente a partir de um parâmetro do construtor, como:

ruim dart
class Point {
  double x, y;
  Point(double x, double y) : x = x, y = y;
}

Aqui temos que digitar x quatro vezes para definir um campo. Podemos fazer melhor:

bom dart
class Point {
  double x, y;
  Point(this.x, this.y);
}

Essa sintaxe this. antes de um parâmetro de construtor é chamada de "parâmetro inicializador". Nem sempre você pode aproveitá-la. Às vezes você quer ter um parâmetro nomeado cujo nome não corresponde ao nome do campo que está inicializando. Mas quando você pode usar parâmetros inicializadores, você deve.

NÃO use late quando uma lista de inicializadores do construtor resolver

#

O Dart exige que você inicialize campos não nullable antes que eles possam ser lidos. Como os campos podem ser lidos dentro do corpo do construtor, isso significa que você recebe um erro se não inicializar um campo não nullable antes de o corpo ser executado.

Você pode fazer esse erro desaparecer marcando o campo como late. Isso transforma o erro de compilação em um erro de tempo de execução se você acessar o campo antes de ele ser inicializado. Em alguns casos é isso que você precisa, mas muitas vezes a solução certa é inicializar o campo na lista de inicializadores do construtor:

bom dart
class Point {
  double x, y;
  Point.polar(double theta, double radius)
    : x = cos(theta) * radius,
      y = sin(theta) * radius;
}
ruim dart
class Point {
  late double x, y;
  Point.polar(double theta, double radius) {
    x = cos(theta) * radius;
    y = sin(theta) * radius;
  }
}

A lista de inicializadores dá acesso aos parâmetros do construtor e permite inicializar campos antes que eles possam ser lidos. Então, se for possível usar uma lista de inicializadores, isso é melhor do que tornar o campo late e perder um pouco de segurança estática e desempenho.

FAÇA usar ; em vez de {} para corpos de construtor vazios

#

ia-translate: true ---Regra do linter: empty_constructor_bodies

No Dart, um construtor com corpo vazio pode ser encerrado apenas com um ponto e vírgula. (Na verdade, isso é obrigatório para construtores const.)

bom dart
class Point {
  double x, y;
  Point(this.x, this.y);
}
ruim dart
class Point {
  double x, y;
  Point(this.x, this.y) {}
}

PREFIRA usar a sintaxe concisa de construtores

#

ia-translate: true ---Regra do linter: unnecessary_type_name_in_constructor

Ao declarar construtores dentro do corpo da classe, use a sintaxe concisa de construtores em vez de repetir o nome da classe. Use factory para construtores de fábrica e new para todos os outros construtores.

Para uma referência completa das formas da sintaxe concisa, consulte a tabela de sintaxe concisa de construtores.

bom dart
class Logger {
  final String name;
​
  factory(String name) => _cache[name] ??= Logger._internal(name);
​
  new _internal(this.name);
  new fromJson(Map<String, Object?> json) : name = json['name'] as String;
​
  static final Map<String, Logger> _cache = {};
}
ruim dart
class Logger {
  final String name;
​
  factory Logger(String name) => _cache[name] ??= Logger._internal(name);
​
  Logger._internal(this.name);
  Logger.fromJson(Map<String, Object?> json) : name = json['name'] as String;
​
  static final Map<String, Logger> _cache = {};
}

Repetir o nome da classe é redundante e dificulta a refatoração caso a classe seja renomeada. Usar new e factory mantém as declarações de construtores concisas e consistentes.

NÃO use new

#

ia-translate: true ---Regra do linter: unnecessary_new

A palavra-chave new é opcional ao chamar um construtor. O significado dela não é claro, porque construtores de fábrica fazem com que uma invocação com new pode não retornar realmente um objeto novo.

A linguagem ainda permite new, mas considere o uso dela obsoleto e evite usá-la no seu código.

bom dart
Widget build(BuildContext context) {
  return Row(
    children: [
      RaisedButton(child: Text('Increment')),
      Text('Click!'),
    ],
  );
}
ruim dart
Widget build(BuildContext context) {
  return new Row(
    children: [
      new RaisedButton(child: new Text('Increment')),
      new Text('Click!'),
    ],
  );
}

NÃO use const de forma redundante

#

ia-translate: true ---Regra do linter: unnecessary_const

Em contextos em que uma expressão precisa ser constante, a palavra-chave const é implícita, não precisa ser escrita e não deve ser. Esses contextos são qualquer expressão dentro de:

  • Um literal de coleção const.
  • Uma chamada de construtor const.
  • Uma anotação de metadados.
  • O inicializador de uma declaração de variável const.
  • Uma expressão de caso de switch, a parte logo após case e antes do :, e não o corpo do caso.

(Valores padrão não estão nesta lista porque versões futuras do Dart podem suportar valores padrão não const.)

Basicamente, em qualquer lugar em que seria um erro escrever new em vez de const, o Dart permite que você omita o const.

bom dart
const primaryColors = [
  Color('red', [255, 0, 0]),
  Color('green', [0, 255, 0]),
  Color('blue', [0, 0, 255]),
];
ruim dart
const primaryColors = const [
  const Color('red', const [255, 0, 0]),
  const Color('green', const [0, 255, 0]),
  const Color('blue', const [0, 0, 255]),
];

Tratamento de erros

#

O Dart usa exceções quando ocorre um erro no seu programa. As boas práticas a seguir se aplicam a capturar e lançar exceções.

EVITE capturas sem cláusulas on

#

ia-translate: true ---Regra do linter: avoid_catches_without_on_clauses

Uma cláusula catch sem qualificador on captura qualquer coisa lançada pelo código no bloco try. Tratamento de exceções "Pokémon" muito provavelmente não é o que você quer. Seu código trata corretamente StackOverflowError ou OutOfMemoryError? Se você passar por engano o argumento errado para um método naquele bloco try, você quer que o depurador aponte o erro ou preferiria que o útil ArgumentError fosse engolido? Você quer que as instruções assert() dentro desse código simplesmente desapareçam, já que você está capturando os AssertionErrors lançados?

A resposta provavelmente é "não", e nesse caso você deve filtrar os tipos que captura. Na maioria dos casos, você deve ter uma cláusula on que o limite aos tipos de falhas em tempo de execução que você conhece e está tratando corretamente.

Em casos raros, você pode querer capturar qualquer erro de tempo de execução. Isso geralmente acontece em código de framework ou de baixo nível que tenta isolar código arbitrário de aplicação para que ele não cause problemas. Mesmo aqui, geralmente é melhor capturar Exception do que capturar todos os tipos. Exception é a classe base para todos os erros de tempo de execução e exclui erros que indicam bugs de programação no código.

NÃO descarte erros em capturas sem cláusulas on

#

Se você realmente sente que precisa capturar tudo que pode ser lançado de uma região de código, faça algo com o que você captura. Registre em log, exiba para o usuário ou relance, mas não descarte silenciosamente.

FAÇA lançar objetos que implementam Error apenas para erros de programação

#

A classe Error é a classe base para erros de programação. Quando um objeto desse tipo, ou de uma de suas subinterfaces como ArgumentError, é lançado, isso significa que há um bug no seu código. Quando sua API quer informar a quem a chamou que está sendo usada incorretamente, lançar um Error transmite esse sinal com clareza.

Por outro lado, se a exceção for algum tipo de falha em tempo de execução que não indica um bug no código, então lançar um Error é enganoso. Em vez disso, lance uma das classes core de Exception ou algum outro tipo.

NÃO capture explicitamente Error ou tipos que o implementam

#

ia-translate: true ---Regra do linter: avoid_catching_errors

Isso decorre do que foi dito acima. Como um Error indica um bug no seu código, ele deve desfazer toda a pilha de chamadas, interromper o programa e imprimir um stack trace para que você possa localizar e corrigir o bug.

Capturar erros desses tipos quebra esse processo e mascara o bug. Em vez de adicionar código de tratamento de erro para lidar com essa exceção depois do fato, volte e corrija o código que faz essa exceção ser lançada, em primeiro lugar.

FAÇA usar rethrow para relançar uma exceção capturada

#

ia-translate: true ---Regra do linter: use_rethrow_when_possible

Se você decidir relançar uma exceção, prefira usar a instrução rethrow em vez de lançar o mesmo objeto de exceção com throw. rethrow preserva o stack trace original da exceção. throw, por outro lado, redefine o stack trace para a última posição lançada.

ruim dart
try {
  somethingRisky();
} catch (e) {
  if (!canHandle(e)) throw e;
  handle(e);
}
bom dart
try {
  somethingRisky();
} catch (e) {
  if (!canHandle(e)) rethrow;
  handle(e);
}

Assincronia

#

O Dart tem vários recursos de linguagem para dar suporte à programação assíncrona. As boas práticas a seguir se aplicam à codificação assíncrona.

PREFIRA async/await em vez de usar futures diretamente

#

Código assíncrono é notoriamente difícil de ler e depurar, mesmo ao usar uma boa abstração como futures. A sintaxe async/await melhora a legibilidade e permite usar todas as estruturas de controle de fluxo do Dart dentro do seu código assíncrono.

bom dart
Future<int> countActivePlayers(String teamName) async {
  try {
    var team = await downloadTeam(teamName);
    if (team == null) return 0;
​
    var players = await team.roster;
    return players.where((player) => player.isActive).length;
  } on DownloadException catch (e) {
    log.error(e);
    return 0;
  }
}
ruim dart
Future<int> countActivePlayers(String teamName) {
  return downloadTeam(teamName)
      .then((team) {
        if (team == null) return Future.value(0);
​
        return team.roster.then((players) {
          return players.where((player) => player.isActive).length;
        });
      })
      .onError<DownloadException>((e, _) {
        log.error(e);
        return 0;
      });
}

NÃO use async quando ele não tem efeito útil

#

É fácil criar o hábito de usar async em qualquer função que faça algo relacionado a assincronia. Mas, em alguns casos, ele é desnecessário. Se você pode omitir o async sem alterar o comportamento da função, faça isso.

bom dart
Future<int> fastestBranch(Future<int> left, Future<int> right) {
  return Future.any([left, right]);
}
ruim dart
Future<int> fastestBranch(Future<int> left, Future<int> right) async {
  return Future.any([left, right]);
}

Os casos em que async é útil incluem:

  • Você está usando await. (Esse é o caso óbvio.)

  • Você está retornando um erro de forma assíncrona. async seguido de throw é mais curto do que return Future.error(...).

  • Você está retornando um valor e quer que ele seja empacotado implicitamente em um future. async é mais curto do que Future.value(...).

bom dart
Future<void> usesAwait(Future<String> later) async {
  print(await later);
}
​
Future<void> asyncError() async {
  throw 'Error!';
}
​
Future<String> asyncValue() async => 'value';

CONSIDERE usar métodos de ordem superior para transformar um stream

#

Isso é paralelo à sugestão acima sobre iterables. Streams suportam muitos dos mesmos métodos e também lidam corretamente com coisas como transmissão de erros, fechamento etc.

EVITE usar Completer diretamente

#

Muitas pessoas que são novas na programação assíncrona querem escrever código que produz um future. Os construtores de Future não parecem atender à necessidade delas, então elas acabam encontrando a classe Completer e a usam.

ruim dart
Future<bool> fileContainsBear(String path) {
  var completer = Completer<bool>();
​
  File(path).readAsString().then((contents) {
    completer.complete(contents.contains('bear'));
  });
​
  return completer.future;
}

Completer é necessário para dois tipos de código de baixo nível: novas primitivas assíncronas e interface com código assíncrono que não usa futures. A maior parte do outro código deve usar async/await ou Future.then(), porque eles são mais claros e facilitam o tratamento de erros.

bom dart
Future<bool> fileContainsBear(String path) {
  return File(path).readAsString().then((contents) {
    return contents.contains('bear');
  });
}
bom dart
Future<bool> fileContainsBear(String path) async {
  var contents = await File(path).readAsString();
  return contents.contains('bear');
}

FAÇA testar Future<T> ao desambiguar um FutureOr<T> cujo argumento de tipo pode ser Object

#

Antes de fazer qualquer coisa útil com um FutureOr<T>, normalmente você precisa fazer uma verificação is para ver se tem um Future<T> ou um T puro. Se o argumento de tipo é algum tipo específico, como em FutureOr<int>, não importa qual teste você use, is int ou is Future<int>. Qualquer um funciona, porque esses dois tipos são disjuntos.

No entanto, se o tipo do valor é Object ou um parâmetro de tipo que possa ser instanciado com Object, então os dois ramos se sobrepõem. Future<Object> em si implementa Object, então is Object ou is T, em que T é algum parâmetro de tipo que pode ser instanciado com Object, retorna verdadeiro mesmo quando o objeto é um future. Em vez disso, teste explicitamente o caso Future:

bom dart
Future<T> logValue<T>(FutureOr<T> value) async {
  if (value is Future<T>) {
    var result = await value;
    print(result);
    return result;
  } else {
    print(value);
    return value;
  }
}
ruim dart
Future<T> logValue<T>(FutureOr<T> value) async {
  if (value is T) {
    print(value);
    return value;
  } else {
    var result = await value;
    print(result);
    return result;
  }
}

No exemplo ruim, se você passar a ele um Future<Object>, ele o trata incorretamente como um valor puro e síncrono.

Encontrou essa página sem tradução ou que precisa de correção? Abra uma issue e nos ajude a manter esse site em PT-BR para ajudar pessoas como você.