跳转到正文
ygjoon
返回

Dart 代码生成详解

编辑文章

简介

代码生成是一种自动创建重复性代码的技术。在 Dart 中,我们可以使用 build_runner 和 source_gen 等工具来实现代码生成。

基本配置

pubspec.yaml 配置

dependencies:
  json_annotation: ^4.8.1

dev_dependencies:
  build_runner: ^2.4.6
  json_serializable: ^6.7.1
  source_gen: ^1.4.0

运行命令

## 一次性生成
dart run build_runner build

## 持续监听并生成
dart run build_runner watch

JSON 序列化

基本用法

import 'package:json_annotation/json_annotation.dart';

part 'user.g.dart';

@JsonSerializable()
class User {
  final String name;
  final int age;
  @JsonKey(name: 'is_student')
  final bool isStudent;
  
  User(this.name, this.age, this.isStudent);
  
  // 从 JSON 转换
  factory User.fromJson(Map<String, dynamic> json) => 
      _$UserFromJson(json);
      
  // 转换为 JSON
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

自定义转换

@JsonSerializable()
class DateTime {
  @JsonKey(
    fromJson: _dateTimeFromEpoch,
    toJson: _dateTimeToEpoch,
  )
  final DateTime timestamp;
  
  static DateTime _dateTimeFromEpoch(int epoch) =>
      DateTime.fromMillisecondsSinceEpoch(epoch);
      
  static int _dateTimeToEpoch(DateTime time) =>
      time.millisecondsSinceEpoch;
}

自定义生成器

Builder 定义

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

Builder customBuilder(BuilderOptions options) =>
    SharedPartBuilder([CustomGenerator()], 'custom');

class CustomGenerator extends Generator {
  @override
  String generate(LibraryReader library, BuildStep buildStep) {
    // 生成代码的逻辑
    final buffer = StringBuffer();
    
    // 遍历所有类
    for (var element in library.allElements) {
      if (element is ClassElement) {
        // 生成相关代码
        buffer.writeln('// Generated code for ${element.name}');
      }
    }
    
    return buffer.toString();
  }
}

build.yaml 配置

targets:
  $default:
    builders:
      custom_builder:
        enabled: true
        generate_for:
          - lib/**/*.dart

完整示例

// entity.dart
import 'package:json_annotation/json_annotation.dart';

part 'entity.g.dart';

@JsonSerializable()
class Product {
  final String id;
  final String name;
  final double price;
  @JsonKey(name: 'created_at')
  final DateTime createdAt;
  @JsonKey(defaultValue: false)
  final bool isAvailable;
  
  const Product({
    required this.id,
    required this.name,
    required this.price,
    required this.createdAt,
    required this.isAvailable,
  });
  
  factory Product.fromJson(Map<String, dynamic> json) =>
      _$ProductFromJson(json);
      
  Map<String, dynamic> toJson() => _$ProductToJson(this);
}

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

class RepositoryGenerator extends Generator {
  @override
  String generate(LibraryReader library, BuildStep buildStep) {
    final buffer = StringBuffer();
    
    for (var element in library.allElements) {
      if (element is ClassElement) {
        // 生成 Repository 类
        buffer.writeln('''
        class ${element.name}Repository {
          final List<${element.name}> _items = [];
          
          void add(${element.name} item) {
            _items.add(item);
          }
          
          void remove(${element.name} item) {
            _items.remove(item);
          }
          
          List<${element.name}> getAll() {
            return List.unmodifiable(_items);
          }
          
          ${element.name}? findById(String id) {
            return _items.firstWhere(
              (item) => item.id == id,
              orElse: () => null,
            );
          }
        }
        ''');
      }
    }
    
    return buffer.toString();
  }
}

// 使用示例
void main() {
  final product = Product(
    id: '1',
    name: 'Example Product',
    price: 99.99,
    createdAt: DateTime.now(),
    isAvailable: true,
  );
  
  // 序列化
  final json = product.toJson();
  print('JSON: $json');
  
  // 反序列化
  final restored = Product.fromJson(json);
  print('Restored: $restored');
  
  // 使用生成的 Repository
  final repository = ProductRepository();
  repository.add(product);
  
  final found = repository.findById('1');
  print('Found product: $found');
}

最佳实践

  1. 合理组织生成代码
  2. 使用适当的注解
  3. 处理好异常情况
  4. 提供清晰的文档
  5. 注意性能影响

注意事项

  1. 生成的代码不要手动修改
  2. 注意代码生成的时机
  3. 处理好依赖关系
  4. 合理使用配置选项
  5. 注意版本兼容性

实战案例:我为什么手写了一个 JSON 转 Dart 工具

json_serializable + build_runner 是标准答案,但我做了个反模式的项目:ygking_json_to_dart_model,一个纯手写的 JSON 转 Dart Model 工具(lib/services/json_converter.dart,143 行),没有用任何代码生成框架。

为什么?因为场景不同。json_serializable 是给工程里用的:model 类先写好,注解一打,build_runner 生成 fromJson。而我的工具是给调试时用的:后端甩过来一段 JSON,我要立刻得到一份能用的 Dart 类,复制粘贴就走。为这个场景引入 build_runner 是杀鸡用牛刀。

手写生成器的几个关键决策(都是踩过坑的):

  1. 字段全部可空(String? name;)。后端 JSON 的字段随时可能缺,手写时如果生成非空字段,fromJson 里一遇到 null 就炸。可空是最务实的选择。
  2. 用 StringBuffer 拼字符串。生成代码是高频字符串拼接,用 + 在 Dart 里会产生大量中间对象。143 行里 buffer.writeln 出现了几十次——这不是洁癖,是真被卡过。
  3. 数组只取第一个元素当模板。JSON 里 "list": [{...}, {...}],我只看 [0] 的结构生成嵌套类。代价是:如果数组里混了不同结构的对象,生成的类是错的。但实战中后端返回的数组 99% 是同构的,这个 trade-off 值得。
  4. 嵌套类名从 key 推导(key.capitalize())。比如 "user": {...} 就生成 User 类。简单粗暴,但够用。

这个项目让我对”代码生成”有了不一样的理解:教程里的代码生成是编译期的、框架驱动的;实战里还有一种是运行时的、字符串模板驱动的。后者听起来很 low,但 for 掉 build_runner 的等待时间,调试效率是真高。工具和框架没有高低,只有场景合不合适。

项目地址:github.com/liudonjun/ygking_json_to_dart_model,json_converter.dart 就 143 行,5 分钟能看完。

总结

代码生成是一种强大的工具,可以帮助我们减少重复性工作,提高开发效率。通过合理使用代码生成工具,可以让代码更加规范和易于维护。


编辑文章
分享这篇文章:

上一篇
Dart 元数据和注解详解
下一篇
Dart Stream 和 Future 详解