Flutter

2 mar, 2026

Metaprogramação em Dart: O Poder do build_runner

Publicidade

Recentemente, a equipe do Flutter trouxe uma surpresa: as Macros, que prometiam uma nova abordagem para a metaprogramação estática, foram descontinuadas. O motivo? O time percebeu que a implementação original degradaria a performance do Dart Analyzer, afetando diretamente a experiência do desenvolvedor (DX).

Essa decisão pode parecer um passo para trás, mas na verdade reforça o compromisso do Flutter com a performance e a usabilidade. Em vez de abandonar a ideia, eles decidiram reaproveitar todo o aprendizado das Macros para melhorar o build_runner, a ferramenta que já conhecemos e que continua sendo a solução para a geração de código em tempo de desenvolvimento.

Mas você sabe como ele funciona? E como criar seu próprio gerador de código, a partir de uma anotação? É isso que vamos ver agora!

Meta Programação

A Meta programação é uma técnica na qual fazemos com que programa (o compilador do dart) leia seu código e gere outro programa (seu app) em tempo de compilação, isso pode ser feito através de recursos de reflexão (reflection).

Então temos Meta Programaçao no Dart? Sim, no Dart é possível utilizar as Mirrors que nos permite gerar código em tempo de compilação, entretanto este recurso foi desabilitado do Dart que o Flutter executa pois o time do Flutter percebeu que certos aspectos que a reflexão iria piorar a experiencia de um aplicativo Flutter.

O Problema da Reflexão no Flutter

Em um ambiente mobile existem pontos extremamente críticos como espaço da memória, performance e consumo de bateria, tornando a sobrecarga desses recursos inaceitáveis e a reflexão é algo que exige que o aplicativo carregue metadados sobre todas suas classes, métodos e variáveis trazendo um aumento considerável do tamanho do artefato final em tempo de execução. Além disso a compilação AOT (Ahead-of-time) também ficaria comprometida pois a reflexão, por sua natureza dinâmica, iria contra os princípios de uma compilação AOT, pois o compilador não consegue otimizar as chamadas de método dinâmicas. Ele precisaria incluir todos os metadados possíveis, o que comprometeria as otimizações.

Além dos pontos já citados tem uma grande preocupação ao tamanho dos binários gerados pois sem a necessidade de incluir os metadados de uma reflexão os artefatos(os arquivos APK, IPA ou appbundle) são significantemente menores e isso facilita bastante a experiencia do usuário que terá a necessidade de fazer o download de sua aplicação.

A Solução: Metaprogramação Estática com build_runner

Com todos esses problemas citados, torna plausível não termos a Meta programação dinâmica em tempo de compilação e é aqui que surge a Meta Programação Estatica, pois desta forma o Flutter/Dart não necessita mais gerar os códigos em tempo de compilação, e agora, ele traz a responsabilidade da geração de código para o desenvolvedor em tempo de desenvolvimento e isso por si só já resolve todos os problemas citados anteriormente

É nesse ponto que o build_runner entra em jogo. Ele lê suas annotations (anotações) no código e, com base nelas, gera o código necessário. Essa abordagem garante que o código final seja otimizado, sem o peso extra dos metadados da reflexão.

O custo, para o desenvolvedor, é a necessidade de executar comandos de build_runner, mas o ganho é um ecossistema mais saudável e performático.

Mão na massa

Então agora vamos realmente ao que interessa, como podemos fazer nossos builders. No exemplo a seguir vamos criar uma tarefa para o build_runner gere o método copyWith da nossa classe

Algo do tipo

class UserModel {
  final String login;
  final String? age;
  final String password;
  static final String teste = 'Teste';

  UserModel({required this.login, required this.password, this.age});

  UserModel copyWith({String? login, String? password, String? age}) {
    return UserModel(
      login ?? this.login,
      password ?? this.password,
      age ?? this.age,
    );
  }
}

Note que temos o método copyWith para todos os atributos da nossa classe, temos atributos nulaveis e atributos estáticos e oque queremos é apenas adicionar a notação DataClass em nossa classe e que gere automaticamente o método copyWith

@DataClass()
class UserModel {
  final String login;
  final String? age;
  final String password;
  static final String teste = 'Teste';

  UserModel({required this.login, required this.password, this.age});
}

Como iremos adicionar um método em uma class já existente partindo de outro arquivo iremos utilizar o recurso das extensões em Dart

class UserModel {
  final String login;
  final String? age;
  final String password;
  static final String teste = 'Teste';

  UserModel({required this.login, required this.password, this.age});
}

extension UserModelDataClass on UserModel {
  UserModel copyWith({String? login, String? password, String? age}) {
    return UserModel(
      login ?? this.login,
      password ?? this.password,
      age ?? this.age,
    );
  }
}

Então a parte do código que teremos que gerar são as extensions.

Press enter or click to view image in full size

Agora vamos criar o builder desse código, para isso, vamos criar um pacote separado que terá as instruções do nosso build_runner, coloquei ele dentro do diretório package, lá dentro bata executar

flutter create — template=package dataclass_generator

Dentro do projeto “dataclass_generator” precisamos adicionar 3 dependências, o build, o analyzer e o source_gen

Execute:

flutter pub add build

flutter pub add analyzer

flutter pub add source_gen

Agora precisamos criar a classe da nossa anotação que será @DataClass(), basta eu criar uma classe comum, vamos adicionar um parâmetro que usaremos para permitir que ele gere ou não o método build_runner

class DataClass {
  final bool generateCopyWith;
  const DataClass({this.generateCopyWith = true});
}

Agora podemos partir para nossa classe responsável por gerar nosso código

A primeira coisa que precisamos fazer é criar um classe que extende de GeneratorForAnnotation<ClasseDeAnotação>

import 'package:source_gen/source_gen.dart';

import '../annotation/data_class.dart';

class DataClassGenerator extends GeneratorForAnnotation<DataClass> {}

E também precisamos criar um método de geração que iremos configurar para ser executado pelo build_runner, ele tem dois parâmetros importantes, o primeiro é uma lista de Geradores (que são as classes que estendem de GeneratorForAnnotation) e o ultimo é qual será o sulfixo do arquivo gerado (lembra do .g.dart?), nesse caso vamos colocar que os arquivos gerados possuirão o sulfixo .dataclass.dart

import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';

import '../annotation/data_class.dart';

Builder dataClassGenerator(BuilderOptions options) =>
    PartBuilder([DataClassGenerator()], '.dataclass.dart');

class DataClassGenerator extends GeneratorForAnnotation<DataClass> {}

Agora vamos sobrescrever o método generateForAnnotatedElement que será responsável por gerar o código a partir de uma anotação e iremos realizar uma anotação inicial para verificar se a anotação @DataClass está vindo a partir de uma Classe, se não for uma classe iremos lançar uma exceção informando que essa anotação só pode ser utilizada de uma classe

import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';

import '../annotation/data_class.dart';

Builder dataClassGenerator(BuilderOptions options) =>
    PartBuilder([DataClassGenerator()], '.dataclass.dart');

class DataClassGenerator extends GeneratorForAnnotation<DataClass> {
  @override
  generateForAnnotatedElement(
    Element element,
    ConstantReader annotation,
    BuildStep buildStep,
  ) {
    if (element is! ClassElement) {
      throw InvalidGenerationSourceError(
        'A anotação @DataClass só pode ser aplicada a classes',
        element: element,
      );
    }
  }
}

Para gerar o código precisamos retornar uma String do código que iremos gerar

Press enter or click to view image in full size

Bom, precisamos deixar esse código mais dinâmico pois tanto os parâmetros quanto os parâmetros dele serão dinâmicos e dependerão da classe que ele estará gerado, então precisamos encontrar padrões e deixar variado o que irá mudar.

Nosso template seria algo do tipo

Press enter or click to view image in full size

Então precisamos apenas coletar o nome da classe e também os atributos da classe e temos essa informação através do parâmetro element.

Press enter or click to view image in full size

Para o nome da classe conseguimos através do atributo name e os campos da classe através do atributo fields e aqui também podemos fazer outra validação para verificar se a classe com o annotation @DataClass não é uma classe sem atributos

import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';

import '../annotation/data_class.dart';

Builder dataClassGenerator(BuilderOptions options) =>
    PartBuilder([DataClassGenerator()], '.dataclass.dart');

class DataClassGenerator extends GeneratorForAnnotation<DataClass> {
  @override
  generateForAnnotatedElement(
    Element element,
    ConstantReader annotation,
    BuildStep buildStep,
  ) {
    if (element is! ClassElement) {
      throw InvalidGenerationSourceError(
        'A anotação @DataClass só pode ser aplicada a classes',
        element: element,
      );
    }

    final className = element.name;
    final fields = element.fields;

    if (fields.isEmpty) {
      throw InvalidGenerationSourceError(
        'A classe $className não tem atributos!',
        element: element,
      );
    }

  }
}

Agora sim podemos construir uma String que irá conter nosso código gerado, como iremos ter que concatenar varias linhas o indicado é utilizar um StringBuffer

Press enter or click to view image in full size

Agora podemos iterar todos os fields da minha classe e gerar a parte dos parâmetros e o construtor da classe

Become a Medium member

Para isso vamos criar 2 listas para armazenar este valor

Press enter or click to view image in full size

E agora sim vamos criar um for in da lista de fields, são 2 informações importantes que iremos precisar nesse momento, a primeira é o nome do campo e a segunda é o tipo do campo, podemos obter ambos através do parâmetro name type respectivamente, onde o parâmetro type precisamos chamar o método getDisplayString()

Press enter or click to view image in full size

Agora podemos adicionar as linhas das duas linhas, onde o construtor ficara

constructor.add(‘$fieldName: $fieldName ?? this.$fieldName’);

e os parâmetrosficará

params.add(‘$fieldType${fieldType.endsWith(‘?’) ? ‘’ : ‘?’} $fieldName’);

note que os parâmetros precisamos verificar se ele já não é nulavel para não adicionar o ? duas vezes

aqui nesse for in também podemos adicionar algumas validações como verificar se o atributo não é estático ou se ele não é um setter

Press enter or click to view image in full size

O código completo do for in ficará

for (final field in fields) {
  if (field.isStatic) continue;
  if (field.setter != null) continue;
  final fieldName = field.name ?? '';
  final fieldType = field.type.getDisplayString();
  params.add(
    '$fieldType${fieldType.endsWith('?') ? '' : '?'} $fieldName',
  );
  constructor.add('$fieldName: $fieldName ?? this.$fieldName');
}

Agora podemos utilizar do template que criamos anteriormente e adicionar no meu stringBuffer o código que quero gerar, os parâmetros e o construtor iremos juntar todos os valores utilizando o .join() e separando por virgula, aqui também podemos verificar o parâmetro da minha annotation DataClass para verificar se o generateCopyWith está marcado como true e retornar o StringBuffer no final da função

Press enter or click to view image in full size

O código completo ficará

import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';

import '../annotation/data_class.dart';

Builder dataClassGenerator(BuilderOptions options) =>
    PartBuilder([DataClassGenerator()], '.dataclass.dart');

class DataClassGenerator extends GeneratorForAnnotation<DataClass> {
  @override
  generateForAnnotatedElement(
    Element element,
    ConstantReader annotation,
    BuildStep buildStep,
  ) {
    if (element is! ClassElement) {
      throw InvalidGenerationSourceError(
        'A anotação @DataClass só pode ser aplicada a classes',
        element: element,
      );
    }

    final className = element.name;
    final fields = element.fields;

    if (fields.isEmpty) {
      throw InvalidGenerationSourceError(
        'A classe $className não tem atributos!',
        element: element,
      );
    }

    final buffer = StringBuffer();
    final params = <String>[];
    final constructor = <String>[];
    for (final field in fields) {
      if (field.isStatic) continue;
      if (field.setter != null) continue;
      final fieldName = field.name ?? '';
      final fieldType = field.type.getDisplayString();

      params.add('$fieldType${fieldType.endsWith('?') ? '' : '?'} $fieldName');

      constructor.add('$fieldName: $fieldName ?? this.$fieldName');
    }

    buffer.writeln();

    buffer.writeln('extension ${className}DataClass on $className {');
    if (annotation.read('generateCopyWith').boolValue) {
      buffer.writeln('  $className copyWith({${params.join(', ')}}) {');
      buffer.writeln('    return $className(${constructor.join(', ')});');
      buffer.writeln('  }');
    }

    buffer.writeln('}');

    return buffer.toString();
  }
}

Pronto agora a classe do Generator está pronta, não se esqueça de exportar o annotation para fora do pacote

Press enter or click to view image in full size

Agora precisamos fazer algumas configurações finais será necessário criar o arquivo build.yaml

builders:
  data_class:
    import: 'package:dataclass_generator/generators/data_class_generator.dart'
    builder_factories: ['dataClassGenerator']
    build_extensions: { '.dart': ['.dataclass.dart'] }
    build_to: source

Aqui temos alguns parâmetros obrigatório

import: é o caminho de importação da classe que contém o generator

build_factories: lista contendo o nome dos métodos de geração (aquele que tem o sulfixo do nome do arquivo que sera gerado)

build_extensions: Um mapa da extensão de entrada para a lista de extensões de saída que podem ser criadas para essa entrada. Isso deve corresponder aos mapas buildExtensions mesclados de cada Builder em builder_factories.

build_to: O local onde os ativos gerados devem ser enviados. O valor “source” significa que o arquivo gerado ficará no mesmo nível do arquivo original

Para mais informações de outros parâmetros veja em https://pub.dev/packages/build_config

Pronto agora o pacote está pronto para ser usado em nosso projeto principal

Configurações no projeto principal.

Precisamos agora no projeto do aplicativo adicionar 2 dependências no pubspec

A primeira é o build_runner que pode ser uma dependência de desenvolvimento

flutter pub add build_runner –dev

A outra é a o pacote que acabamos de criar, como ele est;a dentro do projeto podemos adicionar através de um path

Press enter or click to view image in full size

Agora podemos criar uma classe adicionando a anotação do DataClass e também informando que parte dó código dessa classe virá através de outro arquivo

Press enter or click to view image in full size

Agora basta executar o build_runner que o arquivo será gerado

Press enter or click to view image in full size

Press enter or click to view image in full size

Otimizando o build_runner

Vale lembrar que build_runner verifica todos arquivos do seu projeto e se seu projeto é muito grande isso pode ser custoso, por mais que a equipe do Flutter esteja otimizando o build_runner nós como desenvolvedores também podemos fazer configurações que possam otimizar o build_runner, criando um arquivo build.yaml

Nele podemos configurar cada tipo de generator e limitar os arquivos que ele irá observar através do parâmetro generate_for, por exemplo, quero que meu dataclass_generator so observe arquivos que tenha o sulfixo _model.dart

targets:
  $default:
    builders:
      dataclass_generator|data_class:
        enabled: true
        generate_for:
          - lib/**/**_model.dart

Viu so como é simples fazer uma metaprogramação estática?

Debugando a Geração de Código

build_runner também pode ser depurado! A primeira vez que você executa o comando flutter pub run build_runner build, ele gera um arquivo temporário em .dart_tool\build\entrypoint\build.dart.

Agora basta configurar o launch.json

Para depurar, basta criar uma configuração no seu launch.json do VS Code que aponte para esse arquivo e adicione seus breakpoints no gerador.

{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "Debug Generator",
            "request": "launch",
            "program": ".dart_tool/build/entrypoint/build.dart",
            "type": "dart",
            "args": [
                "build"
            ]
        }
    ]
}

E agora só executar

Press enter or click to view image in full size

E voilá, conseguimos debugar o método de geração

Incrível, né? É um processo que parece complexo, mas é bem simples de seguir.

E aí, achou a decisão de focar no build_runner a mais acertada?

Se quiser conferir o código completo está lá no meu Github