Melhores Práticas de Revisão de Código Flutter/Dart
Lista de verificação abrangente e independente de biblioteca para revisar aplicações Flutter/Dart. Esses princípios se aplicam independentemente da solução de gerenciamento de estado, biblioteca de roteamento ou framework de DI utilizado.
1. Saúde Geral do Projeto
- O projeto segue uma estrutura de pastas consistente (feature-first ou layer-first)
- Separação adequada de responsabilidades: UI, lógica de negócios, camadas de dados
- Nenhuma lógica de negócios em widgets; widgets são puramente de apresentação
-
pubspec.yamlestá limpo — sem dependências não utilizadas, versões fixadas adequadamente -
analysis_options.yamlinclui um conjunto de lint rigoroso com configurações de análise estritas ativadas - Nenhuma instrução
print()em código de produção — usedart:developerlog()ou um pacote de logging - Arquivos gerados (
.g.dart,.freezed.dart,.gr.dart) estão atualizados ou em.gitignore - Código específico de plataforma isolado por trás de abstrações
2. Armadilhas da Linguagem Dart
- dynamic implícito: Anotações de tipo ausentes levando a
dynamic— ativestrict-casts,strict-inference,strict-raw-types - Mau uso do null safety: Uso excessivo de
!(operador bang) em vez de verificações de nulo adequadas ou pattern matching do Dart 3 (if (value case var v?)) - Falhas de promoção de tipo: Usar
this.fieldonde a promoção de variável local funcionaria - Captura muito ampla:
catch (e)sem cláusulaon; sempre especifique os tipos de exceção - Capturar
Error: Subtipos deErrorindicam bugs e não devem ser capturados -
asyncnão utilizado: Funções marcadas comoasyncque nunca usamawait— overhead desnecessário - Uso excessivo de
late:lateusado onde nullable ou inicialização de construtor seria mais segura; adia erros para tempo de execução - Concatenação de strings em loops: Use
StringBufferem vez de+para construção iterativa de strings - Estado mutável em contextos
const: Campos em classes de construtorconstnão devem ser mutáveis - Ignorar valores de retorno de
Future: Useawaitou chame explicitamenteunawaited()para sinalizar intenção -
varondefinalfunciona: Prefirafinalpara variáveis locais econstpara constantes de tempo de compilação - Imports relativos: Use imports
package:para consistência - Coleções mutáveis expostas: APIs públicas devem retornar visões não modificáveis, não
List/Mapbrutos - Falta de pattern matching do Dart 3: Prefira expressões switch e
if-caseem vez de verificaçõesisverbosas e casting manual - Classes descartáveis para múltiplos retornos: Use registros Dart 3
(String, int)em vez de DTOs de uso único -
print()em código de produção: Usedart:developerlog()ou o pacote de logging do projeto;print()não tem níveis de log e não pode ser filtrado
3. Melhores Práticas de Widgets
Decomposição de widgets:
- Nenhum widget único com um método
build()excedendo ~80-100 linhas - Widgets divididos por encapsulamento E por como eles mudam (limites de reconstrução)
- Métodos auxiliares privados
_build*()que retornam widgets são extraídos para classes de widget separadas (permite reuso de elementos, propagação const e otimizações do framework) - Stateless widgets preferidos em vez de Stateful quando nenhum estado local mutável é necessário
- Widgets extraídos estão em arquivos separados quando reutilizáveis
Uso de const:
- Construtores
constusados sempre que possível — previne reconstruções desnecessárias - Literais
constpara coleções que não mudam (const [],const {}) - Construtor é declarado como
constquando todos os campos são finais
Uso de Key:
-
ValueKeyusado em listas/grids para preservar estado entre reordenações -
GlobalKeyusado com moderação — apenas quando acessar estado através da árvore é realmente necessário -
UniqueKeyevitado embuild()— força reconstrução a cada frame -
ObjectKeyusado quando a identidade é baseada em um objeto de dados em vez de um único valor
Tema & sistema de design:
- Cores vêm de
Theme.of(context).colorScheme— semColors.redou valores hexadecimais codificados - Estilos de texto vêm de
Theme.of(context).textTheme— semTextStyleinline com tamanhos de fonte brutos - Compatibilidade com modo escuro verificada — sem suposições sobre fundo claro
- Espaçamento e dimensionamento usam tokens de design consistentes ou constantes, não números mágicos
Complexidade do método build:
- Nenhuma chamada de rede, E/S de arquivo ou computação pesada em
build() - Nenhum
Future.then()ou trabalhoasyncembuild() - Nenhuma criação de inscrição (
.listen()) embuild() -
setState()localizado na menor subárvore possível
4. Gerenciamento de Estado (Independente de Biblioteca)
Esses princípios se aplicam a todas as soluções de gerenciamento de estado do Flutter (BLoC, Riverpod, Provider, GetX, MobX, Signals, ValueNotifier, etc.).
Arquitetura:
- A lógica de negócios reside fora da camada de widgets — em um componente de gerenciamento de estado (BLoC, Notifier, Controller, Store, ViewModel, etc.)
- Gerenciadores de estado recebem dependências via injeção, não as construindo internamente
- Uma camada de serviço ou repositório abstrai fontes de dados — widgets e gerenciadores de estado não devem chamar APIs ou bancos de dados diretamente
- Gerenciadores de estado têm uma única responsabilidade — nenhum gerenciador "deus" lidando com preocupações não relacionadas
- Dependências entre componentes seguem as convenções da solução:
- Em Riverpod: providers dependendo de providers via
ref.watché esperado — sinalize apenas cadeias circulares ou excessivamente emaranhadas - Em BLoC: blocs não devem depender diretamente de outros blocs — prefira repositórios compartilhados ou coordenação na camada de apresentação
- Em outras soluções: siga as convenções documentadas para comunicação entre componentes
- Em Riverpod: providers dependendo de providers via
Imutabilidade & igualdade de valor (para soluções de estado imutável: BLoC, Riverpod, Redux):
- Objetos de estado são imutáveis — novas instâncias criadas via
copyWith()ou construtores, nunca mutadas no lugar - Classes de estado implementam
==ehashCodeadequadamente (todos os campos incluídos na comparação) - O mecanismo é consistente em todo o projeto — override manual,
Equatable,freezed, registros Dart, ou outros - Coleções dentro de objetos de estado não são expostas como
List/Mapmutáveis brutos
Disciplina de reatividade (para soluções de mutação reativa: MobX, GetX, Signals):
- O estado só é mutado através da API reativa da solução (
@actionem MobX,.valueem signals,.obsem GetX) — mutação direta de campo ignora o rastreamento de mudanças - Valores derivados usam o mecanismo computado da solução em vez de serem armazenados redundantemente
- Reações e descartadores são devidamente limpos (
ReactionDisposerem MobX, limpeza de efeito em Signals)
Design da forma do estado:
- Estados mutuamente exclusivos usam tipos selados, variantes de união, ou o tipo de estado assíncrono embutido da solução (ex:
AsyncValuedo Riverpod) — não flags booleanas (isLoading,isError,hasData) - Cada operação assíncrona modela carregamento, sucesso e erro como estados distintos
- Todas as variantes de estado são tratadas exaustivamente na UI — nenhum caso ignorado silenciosamente
- Estados de erro carregam informações de erro para exibição; estados de carregamento não carregam dados obsoletos
- Dados anuláveis não são usados como indicador de carregamento — estados são explícitos
// RUIM — sopa de flags booleanas permite estados impossíveis
class UserState {
bool isLoading = false;
bool hasError = false; // isLoading && hasError é representável!
User? user;
}
// BOM (abordagem imutável) — tipos selados tornam estados impossíveis irrepresentáveis
sealed class UserState {}
class UserInitial extends UserState {}
class UserLoading extends UserState {}
class UserLoaded extends UserState {
final User user;
const UserLoaded(this.user);
}
class UserError extends UserState {
final String message;
const UserError(this.message);
}
// BOM (abordagem reativa) — enum observável + dados, mutações via API de reatividade
// enum UserStatus { initial, loading, loaded, error }
// Use o sinal/observável da sua solução para encapsular status e dados separadamente
Otimização de reconstrução:
- Widgets consumidores de estado (Builder, Consumer, Observer, Obx, Watch, etc.) com escopo o mais estreito possível
- Seletores usados para reconstruir apenas quando campos específicos mudam — não a cada emissão de estado
- Widgets
constusados para parar a propagação de reconstrução através da árvore - Estado computado/derivado é calculado reativamente, não armazenado redundantemente
Inscrições & descarte:
- Todas as inscrições manuais (
.listen()) são canceladas emdispose()/close() - Stream controllers são fechados quando não são mais necessários
- Timers são cancelados no ciclo de vida de descarte
- Ciclo de vida gerenciado pelo framework é preferido em vez de inscrição manual (construtores declarativos em vez de
.listen()) - Verificação
mountedantes desetStateem callbacks assíncronos -
BuildContextnão usado apósawaitsem verificarcontext.mounted(Flutter 3.7+) — contexto obsoleto causa crashes - Nenhuma navegação, diálogos ou mensagens de scaffold após lacunas assíncronas sem verificar se o widget ainda está montado
-
BuildContextnunca armazenado em singletons, gerenciadores de estado ou campos estáticos
Estado local vs global:
- Estado de UI efêmero (checkbox, slider, animação) usa estado local (
setState,ValueNotifier) - Estado compartilhado é elevado apenas o necessário — não superglobalizado
- Estado com escopo de funcionalidade é devidamente descartado quando a funcionalidade não está mais ativa
5. Desempenho
Reconstruções desnecessárias:
-
setState()não chamado no nível do widget raiz — localize mudanças de estado - Widgets
constusados para parar a propagação de reconstrução -
RepaintBoundaryusado em torno de subárvores complexas que repintam independentemente - Parâmetro
childdeAnimatedBuilderusado para subárvores independentes da animação
Operações caras em build():
- Nenhuma ordenação, filtragem ou mapeamento de grandes coleções em
build()— compute na camada de gerenciamento de estado - Nenhuma compilação de regex em
build() - Uso de
MediaQuery.of(context)é específico (ex:MediaQuery.sizeOf(context))
Otimização de imagens:
- Imagens de rede usam cache (qualquer solução de cache apropriada para o projeto)
- Resolução de imagem apropriada para o dispositivo alvo (não carregar imagens 4K para miniaturas)
-
Image.assetcomcacheWidth/cacheHeightpara decodificar no tamanho de exibição - Widgets de placeholder e erro fornecidos para imagens de rede
Carregamento sob demanda:
-
ListView.builder/GridView.builderusado em vez deListView(children: [...])para listas grandes ou dinâmicas (construtores concretos são aceitáveis para listas pequenas e estáticas) - Paginação implementada para grandes conjuntos de dados
- Carregamento adiado (
deferred as) usado para bibliotecas pesadas em builds web
Outros:
- Widget
Opacityevitado em animações — useAnimatedOpacityouFadeTransition - Clipping evitado em animações — pré-recorte imagens
-
operator ==não sobrescrito em widgets — use construtoresconstem vez disso - Widgets de dimensão intrínseca (
IntrinsicHeight,IntrinsicWidth) usados com moderação (passo de layout extra)
6. Testes
Tipos de teste e expectativas:
- Testes de unidade: Cobrem toda a lógica de negócios (gerenciadores de estado, repositórios, funções utilitárias)
- Testes de widget: Cobrem comportamento individual de widgets, interações e saída visual
- Testes de integração: Cobrem fluxos críticos do usuário de ponta a ponta
- Testes golden: Comparações pixel-perfeitas para componentes de UI críticos para o design
Metas de cobertura:
- Mire em 80%+ de cobertura de linhas na lógica de negócios
- Todas as transições de estado têm testes correspondentes (carregando → sucesso, carregando → erro, tentar novamente, etc.)
- Casos de borda testados: estados vazios, estados de erro, estados de carregamento, valores de limite
Isolamento de teste:
- Dependências externas (clientes de API, bancos de dados, serviços) são mockadas ou falsificadas
- Cada arquivo de teste testa exatamente uma classe/unidade
- Testes verificam comportamento, não detalhes de implementação
- Stubs definem apenas o comportamento necessário para cada teste (stubbing mínimo)
- Nenhum estado mutável compartilhado entre casos de teste
Qualidade do teste de widget:
-
pumpWidgetepumpusados corretamente para operações assíncronas -
find.byType,find.text,find.byKeyusados adequadamente - Nenhum teste instável dependente de temporização — use
pumpAndSettleoupump(Duration)explícito - Testes executados em CI e falhas bloqueiam merges
7. Acessibilidade
Widgets semânticos:
- Widget
Semanticsusado para fornecer rótulos de leitor de tela onde rótulos automáticos são insuficientes -
ExcludeSemanticsusado para elementos puramente decorativos -
MergeSemanticsusado para combinar widgets relacionados em um único elemento acessível - Imagens têm a propriedade
semanticLabeldefinida
Suporte a leitor de tela:
- Todos os elementos interativos são focáveis e têm descrições significativas
- A ordem de foco é lógica (segue a ordem de leitura visual)
Acessibilidade visual:
- Taxa de contraste >= 4.5:1 para texto contra fundo
- Alvos tocáveis têm pelo menos 48x48 pixels
- Cor não é o único indicador de estado (use ícones/texto junto)
- Texto escala com as configurações de tamanho de fonte do sistema
Acessibilidade de interação:
- Nenhum callback
onPressedsem operação — todo botão faz algo ou está desabilitado - Campos de erro sugerem correções
- O contexto não muda inesperadamente enquanto o usuário está inserindo dados
8. Preocupações Específicas de Plataforma
Diferenças iOS/Android:
- Widgets adaptativos de plataforma usados onde apropriado
- Navegação de volta tratada corretamente (botão voltar Android, deslizar para voltar iOS)
- Barra de status e área segura tratadas via widget
SafeArea - Permissões específicas de plataforma declaradas em
AndroidManifest.xmleInfo.plist
Design responsivo:
-
LayoutBuilderouMediaQueryusado para layouts responsivos - Breakpoints definidos consistentemente (telefone, tablet, desktop)
- Texto não transborda em telas pequenas — use
Flexible,Expanded,FittedBox - Orientação paisagem testada ou explicitamente bloqueada
- Específico para web: interações mouse/teclado suportadas, estados de hover presentes
9. Segurança
Armazenamento seguro:
- Dados sensíveis (tokens, senhas) armazenados usando mecanismos seguros da plataforma (
flutter_secure_storage,Keychain,Keystore) - Nenhum dado sensível armazenado em
SharedPreferences, arquivos de texto simples ou dentro do código como constantes - HTTPS usado para todas as comunicações de rede — sem exceções
Validação de entrada:
- Todas as entradas do usuário são validadas (formato, comprimento, intervalo) antes do processamento
- Nenhuma injeção de dados não confiáveis diretamente em consultas SQL, snippets HTML ou eval()
- Sanitização de entrada aplicada para XSS (Cross-Site Scripting) em builds web
Autenticação e autorização:
- Tokens são armazenados com segurança e expirados adequadamente
- Verificações de autorização são aplicadas no backend, não apenas na interface do usuário
- Nenhuma chave de API exposta no código do lado do cliente sem ofuscação ou backend proxy
10. Tratamento de Erros e Resiliência
- Erros não são engolidos silenciosamente — cada bloco catch lida ou relança
- Mecanismos de fallback para quando dados externos não podem ser carregados (dados em cache, telas de erro)
- Estratégias de retry para operações de rede que podem ser transitórias
- Timeouts configurados para todas as chamadas de rede e operações de banco de dados
- Blocos
try-catchusados para código que pode lançar, com escopo o mais estreito possível -
FlutterError.onErrorePlatformDispatcher.instance.onErrorconfigurados para tratamento global de erros - Tratamento de erros não expõe detalhes de implementação ao usuário
- Erros são registrados com contexto suficiente para depuração
Regras do Linter Recomendadas
Adicione estas regras ao seu analysis_options.yaml para impor automaticamente muitas dessas verificações:
linter:
rules:
- always_declare_return_types
- avoid_bool_literals_in_conditional_expressions
- avoid_catches_without_on_clauses
- avoid_catching_errors
- avoid_classes_with_only_static_members
- avoid_dynamic_calls
- avoid_equals_and_hash_code_on_mutable_classes
- avoid_implementing_value_types
- avoid_init_to_null
- avoid_null_checks_in_equality_operators
- avoid_print
- avoid_redundant_argument_values
- avoid_returning_null
- avoid_shadowing_type_parameters
- avoid_type_to_string
- avoid_unnecessary_containers
- avoid_unused_constructor_parameters
- avoid_void_async
- cancel_subscriptions
- close_sinks
- comment_references
- constant_identifier_names
- curly_braces_in_flow_control_structures
- directives_ordering
- discarded_futures
- do_not_use_environment
- empty_constructor_bodies
- exhaustive_cases
- file_names
- flutter_style_todos
- implementation_imports
- join_return_with_assignment
- leading_newlines_in_multiline_strings
- library_names
- library_prefixes
- lines_longer_than_80_chars
- missing_whitespace_between_adjacent_strings
- no_adjacent_strings_in_list
- no_duplicate_case_values
- no_leading_underscores_for_local_identifiers
- no_runtimeType_toString
- no_self_assignments
- no_wildcard_variable_uses
- non_constant_identifier_names
- one_member_abstracts
- only_throw_errors
- overridden_fields
- package_api_docs
- prefer_asserts_in_initializer_lists
- prefer_asserts_with_message
- prefer_const_constructors
- prefer_const_constructors_in_immutables
- prefer_const_declarations
- prefer_const_literals_to_create_immutables
- prefer_constructors_over_static_methods
- prefer_double_quotes
- prefer_expression_function_bodies
- prefer_final_fields
- prefer_final_locals
- prefer_final_in_for_each
- prefer_for_elements_to_map_fromIterable
- prefer_foreach
- prefer_function_declarations_over_variables
- prefer_generic_function_type_aliases
- prefer_if_elements_to_conditional_expressions
- prefer_if_null_operators
- prefer_initializing_formals
- prefer_inlined_adds
- prefer_int_literals
- prefer_interpolation_to_compose_strings
- prefer_is_empty
- prefer_is_not_empty
- prefer_is_not_operator
- prefer_iterable_whereType
- prefer_mixin
- prefer_null_aware_method_calls
- prefer_null_aware_operators
- prefer_relative_imports
- prefer_single_quotes
- prefer_spread_collections
- prefer_typing_uninitialized_variables
- prefer_void_to_null
- provide_deprecation_message
- recursive_getters
- require_trailing_commas
- sized_box_for_whitespace
- sort_child_properties_last
- sort_constructors_first
- sort_pub_dependencies
- sort_unnamed_constructors_first
- test_types_in_equals
- throw_in_finally
- tighten_type_of_initializing_formals
- type_annotate_public_apis
- type_init_formals
- unawaited_futures
- unnecessary_await_in_return
- unnecessary_brace_in_string_interps
- unnecessary_const
- unnecessary_final
- unnecessary_getters_setters
- unnecessary_lambdas
- unnecessary_late
- unnecessary_new
- unnecessary_null_aware_assignments
- unnecessary_null_checks
- unnecessary_null_in_if_null_operators
- unnecessary_nullable_for_final_variable_declarations
- unnecessary_overrides
- unnecessary_raw_strings
- unnecessary_statements
- unnecessary_string_escapes
- unnecessary_string_interpolations
- unnecessary_this
- unnecessary_to_list_in_spreads
- use_build_context_synchronously
- use_decorated_box
- use_enums
- use_full_hex_values_for_flutter_colors
- use_function_type_syntax_for_parameters
- use_if_null_to_convert_nulls_to_bools
- use_is_even_rather_than_modulo
- use_key_in_widget_constructors
- use_late_for_private_fields_and_variables
- use_named_constants
- use_raw_strings
- use_rethrow_when_possible
- use_setters_to_change_properties
- use_string_buffers
- use_super_parameters
- use_test_throws_matchers
- use_to_and_as_if_applicable
- valid_regexps
- void_checks
💡 Dica: Não adicione cegamente todas essas regras sem entender seu impacto. Analise cada uma no contexto do seu projeto, discuta com sua equipe e adote-as gradualmente.