Revisão de Código em Flutter: Um Guia Prático

Fazer code review em Flutter costuma parecer simples enquanto o projeto ainda está pequeno. A tela abre, o build passa, o PR parece controlado e a mudança segue. Só que isso muda rápido. Conforme o app cresce, algumas decisões que pareciam inofensivas começam a pesar. Um widget passa a carregar responsabilidade demais, o estado começa a se espalhar, a navegação fica mais difícil de acompanhar e até uma mudança pequena passa a exigir contexto demais.

É por isso que review em Flutter não deveria ser tratado como uma etapa mecânica de aprovação. Ele ajuda a impedir que a codebase fique mais cara de entender, testar e manter com o tempo. Em muitos casos, é justamente nesse momento que ainda dá para corrigir o problema sem transformar a próxima sprint em retrabalho.

Se o seu time revisa PRs de Flutter com frequência, alguns problemas costumam se repetir: lógica demais dentro de widgets, estado difícil de seguir, rebuilds desnecessários, erros de lifecycle, e testes que existem mas não protegem o risco real da mudança.

Checklist rápido de code review em Flutter

  • O widget está carregando mais responsabilidade do que deveria?
  • O gerenciamento de estado continua claro e consistente com o resto do app?
  • Existem rebuilds desnecessários ou trabalho pesado dentro de build()?
  • Async, lifecycle e null safety estão sendo tratados com cuidado?
  • O teste protege de fato o comportamento que mudou?
O que revisarPor que isso importa em Flutter
Arquitetura e responsabilidade dos widgetsEvita que a tela vire o lugar onde renderização, lógica de negócio e acesso a dados ficam misturados
Gerenciamento de estadoDeixa os fluxos mais fáceis de entender, testar e evoluir conforme o app cresce
Performance e rebuildsAjuda a evitar renderizações desnecessárias, build() pesados e lentidão acumulada na interface
Async, lifecycle e null safetyPega problemas que parecem inofensivos no PR, mas depois viram crash, leak ou comportamento inconsistente
Testes e navegaçãoProtege o comportamento que realmente mudou e reduz regressão em fluxos mais sensíveis

Quando a interface começa a concentrar coisa demais

Em Flutter, é comum a camada de UI começar simples e, aos poucos, virar o lugar onde tudo acontece. O widget renderiza, chama serviço, trata loading, faz transformação de dados, controla estado local e ainda decide navegação. No começo, isso até parece prático. Depois, a leitura piora e qualquer ajuste exige cuidado demais.

É por isso que o review precisa olhar além do resultado visual. Não basta saber se a tela funciona. Também precisa ficar claro se aquela implementação continua saudável quando o projeto tiver mais casos, mais fluxo e mais gente mexendo no mesmo app.

O primeiro filtro é entender quanta responsabilidade ficou no widget

Quando um widget começa a carregar responsabilidade demais, o custo aparece rápido. O método build() cresce, a árvore fica cansativa de ler e os callbacks começam a concentrar lógica que deveria estar em outro lugar.

Alguns sinais costumam aparecer juntos:

  • regra de negócio misturada com renderização
  • transformação de dados acontecendo dentro da tela
  • widgets aninhados demais
  • muitos blocos condicionais dentro da árvore
  • callbacks que fazem fetch, validação e atualização de estado ao mesmo tempo

Exemplo: widget fazendo coisa demais

class ProfileScreen extends StatefulWidget {
  const ProfileScreen({super.key});

  @override
  State<ProfileScreen> createState() => _ProfileScreenState();
}

class _ProfileScreenState extends State<ProfileScreen> {
  bool isLoading = false;
  User? user;

  Future<void> loadUser() async {
    setState(() => isLoading = true);
    final response = await api.getUser();
    user = User.fromJson(response.data);
    setState(() => isLoading = false);
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: isLoading
          ? const CircularProgressIndicator()
          : Text(user?.name ?? ''),
    );
  }
}

Esse código pode passar em um review mais apressado, mas a tela já ficou responsável por fetch, transformação da resposta, controle de loading e renderização. Quando entrar tratamento de erro, retry, analytics ou cache, esse trecho vai ficar mais difícil de sustentar.

Nesse caso, um comentário útil de review seria:

“Essa tela já está concentrando renderização, fetch e transformação de dados. Se essa lógica for para uma camada de estado ou serviço, o fluxo fica mais previsível e o teste fica mais simples.”

Esse tipo de comentário ajuda porque ataca o problema real, em vez de ficar em observação genérica sobre limpeza de código.

Quando a lógica de negócio vai parar na UI, a manutenção fica mais cara

Esse é um padrão bem comum em apps Flutter. A entrega sai rápido, o PR parece objetivo e ninguém bloqueia. Só que, pouco tempo depois, aquela tela já ficou dependente de comportamento demais.

Quando a interface busca dado, interpreta resposta, trata exceção e decide regra de negócio ao mesmo tempo, o código perde clareza. Além disso, testar fica mais chato e reaproveitar parte do fluxo vira um trabalho desnecessário.

Separar responsabilidade aqui não é exagero. É uma forma de evitar que a tela vire o ponto onde tudo se mistura.

Estado mal resolvido costuma atrapalhar mais do que o layout

Em muitos apps Flutter, o problema não começa na interface em si. Ele aparece quando ninguém consegue entender com clareza quem muda o quê, em que momento e com qual efeito colateral.

Uma tela lê estado de um lugar, atualiza em outro, dispara side effect em um terceiro ponto e depende de comportamento implícito para continuar funcionando. A aplicação continua rodando, mas qualquer mudança fica mais cara.

Por isso, o review precisa olhar com atenção para esse fluxo:

  • está claro de onde o estado vem?
  • a transição entre loading, sucesso e erro faz sentido?
  • a tela depende demais de estado local improvisado?
  • a solução continua alinhada com o padrão que o restante do app já usa?

Exemplo: estado local demais para uma tela que vai crescer

class CheckoutScreen extends StatefulWidget {
  const CheckoutScreen({super.key});

  @override
  State<CheckoutScreen> createState() => _CheckoutScreenState();
}

class _CheckoutScreenState extends State<CheckoutScreen> {
  bool isLoading = false;
  bool hasCoupon = false;
  String? errorMessage;
  PaymentMethod? selectedMethod;

  Future<void> submitOrder() async {
    setState(() => isLoading = true);

    try {
      await checkoutService.submit(
        hasCoupon: hasCoupon,
        paymentMethod: selectedMethod,
      );
    } catch (e) {
      errorMessage = 'Could not finish order';
    }

    setState(() => isLoading = false);
  }
}

Aqui o problema não é estilo. O fluxo já está carregando estados demais dentro da própria tela. Se entra validação extra, retry, split payment ou regra nova de negócio, a manutenção começa a sofrer.

É justamente nesse tipo de caso que o review precisa agir cedo. Se a tela já está crescendo torta, esperar mais algumas entregas só piora o custo de arrumar.

Problema de performance quase sempre entra devagar

Nem todo problema de performance em Flutter aparece como algo gritante. Na maior parte das vezes, ele se acumula em escolhas pequenas. Um rebuild maior do que precisava, uma lista pouco eficiente, trabalho pesado dentro do build(), uso descuidado de const.

O review não precisa virar uma caça obsessiva por micro otimização. Ainda assim, ele deveria bloquear decisões que, repetidas várias vezes, acabam deixando o app mais pesado do que precisava.

Exemplo: mais rebuild do que a tela precisa

@override
Widget build(BuildContext context) {
  final cart = context.watch<CartProvider>();

  return Column(
    children: [
      Header(total: cart.total),
      ShippingSummary(address: cart.address),
      PaymentSection(method: cart.paymentMethod),
      RecommendedProducts(items: cart.recommendations),
      FooterButton(onPressed: cart.checkout),
    ],
  );
}

Se qualquer mudança no CartProvider reconstruir essa coluna inteira, o custo cresce rápido em telas maiores. Em muitos cenários, separar melhor os pontos de escuta já melhora legibilidade e também reduz renderização desnecessária.

Async, null safety e ciclo de vida pedem mais cuidado do que costuma parecer

Esse é o tipo de detalhe que passa fácil quando o review está corrido. O código parece correto à primeira vista, o fluxo roda e ninguém vê erro no teste manual. Mesmo assim, o problema já pode estar lá.

Alguns exemplos aparecem com frequência:

  • uso excessivo de !
  • tratamento incompleto de erro assíncrono
  • loading mal resolvido
  • controller sem dispose()
  • stream ou subscription esquecida
  • update de estado depois que o widget saiu da árvore

Exemplo: controller criado sem descarte

class _LoginFormState extends State<LoginForm> {
  final emailController = TextEditingController();
  final passwordController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(controller: emailController),
        TextField(controller: passwordController),
      ],
    );
  }
}

Esse é um caso simples e comum. O formulário funciona, mas os controllers ficaram sem dispose(). Em tela pequena isso passa fácil. Em um app maior, esse tipo de detalhe pode gerar leak e comportamento estranho ao longo do tempo.

A correção é direta:

class _LoginFormState extends State<LoginForm> {
  final emailController = TextEditingController();
  final passwordController = TextEditingController();

  @override
  void dispose() {
    emailController.dispose();
    passwordController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(controller: emailController),
        TextField(controller: passwordController),
      ],
    );
  }
}

Esse é exatamente o tipo de problema que um review atento consegue barrar cedo.

Navegação ruim quase nunca nasce de uma vez

Em projetos maiores, a navegação costuma ficar mais difícil aos poucos. Entra uma condição a mais, depois um redirecionamento extra, depois uma exceção de fluxo e, quando o time percebe, a tela já está decidindo navegação demais.

O risco aqui não é só deixar o código feio. O risco é criar um fluxo difícil de testar e difícil de alterar quando produto mudar alguma etapa importante.

Teste só ajuda quando cobre o que realmente mudou

Em Flutter, é fácil aceitar teste como checklist. O PR tem teste, então parece suficiente. Só que a pergunta mais útil aqui é outra: esse teste protege a parte sensível da mudança?

Se a alteração mexe em tratamento de erro, é isso que o teste precisa cobrir. Se mexe em estado ou navegação, o teste precisa proteger esse comportamento. Caso contrário, o time ganha volume e continua exposto no ponto mais delicado.

Exemplo: teste existe, mas deixa o risco principal de fora

Se um PR muda o comportamento de erro de carregamento de uma tela, mas o teste novo só verifica se o título aparece na interface, o problema continua sem cobertura.

Nesse caso, o review precisa ser direto. Não falta teste de forma genérica. Falta teste no lugar em que a regressão realmente pode acontecer.

Ferramentas de AI code review ajudam mais quando entram antes da revisão humana

Em times que revisam muitos PRs, IA costuma ajudar bastante no primeiro passe. Ela reforça regra do time, pega padrão repetitivo e reduz parte do ruído antes da revisão humana.

No contexto de Flutter, isso costuma ser útil em situações como:

  • problemas de ciclo de vida
  • uso inconsistente de estado
  • padrões de arquitetura que o time já decidiu bloquear
  • trechos que fogem do padrão do projeto
  • riscos simples que aparecem em muitos PRs

Um exemplo prático com a Kodus

A Kodus permite criar regras customizadas em linguagem natural direto no app ou dentro do repositório com arquivos markdown em caminhos como .kody/rules/**/*.md ou rules/**/*.md, definindo escopo, severidade, linguagem e instruções de review. Fonte: Repository Rules

Um exemplo útil para times Flutter é uma regra para detectar controllers e subscriptions criados em State sem descarte em dispose().

---
title: "Controllers and subscriptions created in State must be disposed"
scope: "file"
path: ["lib/**/*.dart"]
severity_min: "high"
languages: ["dart"]
buckets: ["performance", "maintainability"]
enabled: true
---

## Instructions
Review Flutter StatefulWidget and State classes for objects that require cleanup.
- Flag TextEditingController, AnimationController, ScrollController, FocusNode, TabController and StreamSubscription created in State and not released in dispose().
- Treat this as high severity because it can cause leaks, stale listeners and hard-to-debug lifecycle issues.
- Ignore cases where ownership clearly belongs to another layer and disposal is handled there.

## Examples

### Bad example
```dart
class _LoginFormState extends State {
final emailController = TextEditingController();

@override
Widget build(BuildContext context) {
return TextField(controller: emailController);
}
}

Good example

class _LoginFormState extends State<LoginForm> {
  final emailController = TextEditingController();

  @override
  void dispose() {
    emailController.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return TextField(controller: emailController);
  }
}

Esse tipo de regra ajuda porque pega um problema recorrente que costuma escapar quando o PR está grande ou quando a revisão está mais corrida.

Checklist prático de code review para Flutter

Se vocês já têm um checklist interno, o melhor caminho costuma ser adaptar esse material ao jeito como o time revisa PR hoje. Para Flutter, eu usaria algo nessa linha.

Arquitetura e organização

  • O PR segue o padrão de arquitetura adotado pelo projeto?
  • As responsabilidades estão bem separadas entre UI, lógica de negócio e acesso a dados?
  • A estrutura de pastas e arquivos continua consistente com o restante do app?
  • A mudança deixou a tela mais acoplada do que precisava?

Estado e navegação

  • O gerenciamento de estado continua consistente com o padrão do projeto?
  • Existe lógica de negócio demais dentro de widgets?
  • O fluxo de loading, sucesso e erro ficou claro?
  • A navegação continua previsível, inclusive em pop, retorno e fluxos mais sensíveis?

UI e experiência

  • A interface reaproveita componentes quando faz sentido?
  • A tela continua responsiva em tamanhos diferentes?
  • O código respeita tema, tipografia e padrões visuais do app?
  • Estados de loading, vazio e erro aparecem de forma clara para quem usa a tela?

Performance

  • O PR evita rebuilds desnecessários?
  • const está sendo usado onde faz sentido?
  • Listas e grids usam builders quando precisam?
  • Existe processamento pesado desnecessário dentro do build()?
  • Prints, logs em excesso e código morto foram removidos?

Async, ciclo de vida e null safety

  • O fluxo assíncrono está claro e trata falha de forma correta?
  • Há uso excessivo de ! ou tratamento frágil de nulos?
  • Controllers, focus nodes e subscriptions são descartados corretamente?
  • Existe risco de atualizar estado depois que o widget saiu da árvore?

Modelos, integração e tratamento de falha

  • Modelos e DTOs estão bem definidos?
  • Parsing e validação de dados fazem sentido para essa mudança?
  • Timeout, erro de rede e estado offline foram considerados quando necessário?
  • A falha dá feedback claro para a pessoa usuária?

Segurança

  • Não há dado sensível hardcoded?
  • Armazenamento seguro está sendo usado quando precisa?
  • Logs não expõem informação sensível?

Testes e qualidade

  • Existem testes para a lógica crítica dessa mudança?
  • Os testes protegem o comportamento alterado de verdade?
  • Tudo passa com dart format e as regras de lint do projeto?
  • Nomes de classes, métodos e variáveis ajudam a entender a intenção do código?

No fim, o review em Flutter afeta mais a manutenção do app do que a aprovação do PR

Quando o review é superficial, o PR entra rápido, mas a conta fica para depois. Por outro lado, quando o time revisa com mais critério, fica mais fácil evitar que a base se torne difícil de entender, testar e evoluir.

Em Flutter isso aparece cedo. Se a base começa a se degradar, o impacto não fica só no código. Ele aparece no ritmo do time, na confiança para mexer em tela antiga e na quantidade de contexto que cada mudança passa a exigir.