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:
library my_library;
part 'some/other/file.dart';
Então o arquivo part deve usar a string URI do arquivo da biblioteca:
part of '../../my_library.dart';
E não o nome da biblioteca:
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:
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óriolib.
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:.
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:
import 'src/stuff.dart';
import 'src/utils.dart';
import '../api.dart';
import 'stuff.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".
Item? bestDeal(List<Item> cart) {
Item? bestItem;
for (final item in cart) {
if (bestItem == null || item.price < bestItem.price) {
bestItem = item;
}
}
return bestItem;
}
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.
void error([String? message]) {
stderr.write(message ?? '\n');
}
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:
if (nonNullableBool) {
...
}
if (!nonNullableBool) {
...
}
if (nonNullableBool == true) {
...
}
if (nonNullableBool == false) {
...
}
Para avaliar uma expressão booleana que é nullable, você deve usar ??
ou uma verificação explícita != null.
// 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) {
...
}
// 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ãonullableBool == truesignifica que a condição avalia parafalse.
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.
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.
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:
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.
raiseAlarm(
'ERROR: Parts of the spaceship are on fire. Other '
'parts are overrun by martians. Unclear which are which.',
);
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:
'Hello, $name! You are ${year - birth} years old.';
'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.
var greeting = 'Hi, $name! I love your ${decade}s costume.';
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:
var points = <Point>[];
var addresses = <String, Address>{};
var counts = <int>{};
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:
var arguments = [
...options,
command,
...?modeFlags,
for (var path in filePaths)
if (path.endsWith('.dart')) path.replaceAll('.dart', '.js'),
];
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.
if (lunchBox.isEmpty) return 'so hungry...';
if (words.isNotEmpty) return words.join(' ');
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.
for (final person in people) {
...
}
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.
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:
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:
// Creates a List<int>:
var iterable = [1, 2, 3];
// Prints "List<int>":
print(iterable.toList().runtimeType);
// 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:
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:
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():
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:
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.
var stuff = <dynamic>[1, 2];
var ints = List<int>.from(stuff);
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.
var stuff = <dynamic>[1, 2];
var reciprocals = stuff.map<double>((n) => n * 2);
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 usandoList.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:
List<int> singletonList(int value) {
var list = <int>[];
list.add(value);
return list;
}
List<int> singletonList(int value) {
var list = []; // List<dynamic>.
list.add(value);
return list.cast<int>();
}
Aqui está convertendo cada elemento no acesso:
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);
}
}
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():
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];
}
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.
void main() {
void localFunction() {
...
}
}
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.
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);
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
finalpara variáveis locais que não são reatribuídas evarpara as que são. -
Use
varpara todas as variáveis locais, mesmo as que não são reatribuídas. Nunca usefinalem locais. (Usarfinalem 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:
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:
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:
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.
class Box {
Object? contents;
}
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.
class Box {
final contents = [];
}
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.
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.
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;
}
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 =>.
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:
class Box {
Object? value;
void clear() {
this.update(null);
}
void update(Object? value) {
this.value = value;
}
}
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:
class ShadeOfGray {
final int brightness;
ShadeOfGray(int val) : brightness = val;
ShadeOfGray.black() : this(0);
// This won't parse or compile!
// ShadeOfGray.alsoBlack() : black();
}
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:
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.
class ProfileMark {
final String name;
final DateTime start;
ProfileMark(this.name) : start = DateTime.now();
ProfileMark.unnamed() : name = '', start = DateTime.now();
}
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:
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:
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:
class Point {
double x, y;
Point.polar(double theta, double radius)
: x = cos(theta) * radius,
y = sin(theta) * radius;
}
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.)
class Point {
double x, y;
Point(this.x, this.y);
}
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.
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 = {};
}
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.
Widget build(BuildContext context) {
return Row(
children: [
RaisedButton(child: Text('Increment')),
Text('Click!'),
],
);
}
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
casee 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.
const primaryColors = [
Color('red', [255, 0, 0]),
Color('green', [0, 255, 0]),
Color('blue', [0, 0, 255]),
];
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.
try {
somethingRisky();
} catch (e) {
if (!canHandle(e)) throw e;
handle(e);
}
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.
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;
}
}
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.
Future<int> fastestBranch(Future<int> left, Future<int> right) {
return Future.any([left, right]);
}
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.
asyncseguido dethrowé mais curto do quereturn Future.error(...). -
Você está retornando um valor e quer que ele seja empacotado implicitamente em um future.
asyncé mais curto do queFuture.value(...).
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.
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.
Future<bool> fileContainsBear(String path) {
return File(path).readAsString().then((contents) {
return contents.contains('bear');
});
}
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:
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;
}
}
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.
Salvo indicação em contrário, a documentação neste site reflete Dart 3.13.7. Página atualizada pela última vez em 2026-11-12. Ver código-fonte ou relatar um problema.
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ê.