Dart: توليد الكود في Dart — build_runner والتسلسل

آخر تحديث: 2026-08-26

توليد الكود هو السلاح النهائي للقضاء على الكود النمطي — دع الآلات تكتب الكود، ودع البشر يكتبون المنطق.

1. ما ستتعلمه


2. قصة مطور حقيقية

(1) نقطة الألم: 3 أيام أمضيت في تسلسل يدوي لـ 20 فئة نموذج

يحتوي خط أنابيب البيانات الخاص ببوب على 20 فئة نموذج بيانات، كل منها يتطلب طرق fromJson/toJson. كتابة كود التسلسل يدوياً لـ 20 فئة استغرقت 3 أيام، حدثت خلالها 4 أخطاء إملائية و 2 من أخطاء تحويل النوع. والأسوأ من ذلك، في كل مرة تتم إضافة حقل جديد، يجب تحديث 3 أماكن في الكود يدوياً (إعلان الحقل، fromJson، toJson). تحديث واحد مفقود تسبب في فشل تحليل 100,000 سجل.

(2) حل json_serializable

علّق على فئة النموذج بـ @JsonSerializable، و build_runner يولد تلقائياً fromJson/toJson. إضافة حقل جديد تتطلب فقط الإعلان + إعادة تشغيل البناء، دون مخاطر الإغفال.

DART
import 'package:json_annotation/json_annotation.dart';
part 'order.g.dart';

@JsonSerializable()
class Order {
  final String id;
  final double amount;
  Order({required this.id, required this.amount});

  factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
  Map<String, dynamic> toJson() => _$OrderToJson(this);
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

(3) الفوائد


3. كيف يعمل build_runner

(1) خط أنابيب التوليد

100%
flowchart LR
  A["model.dart<br/>@JsonSerializable"] --> B[build_runner]
  B --> C["model.g.dart<br/>fromJson/toJson"]
  B --> D["model.freezed.dart<br/>copyWith/equals"]
  A --> E[".part directive"]
  E --> C
  subgraph خط أنابيب التوليد
    B
    C
    D
  end
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
المكون المسؤولية
Builder يقرأ ملفات المصدر، يحدد ما يجب توليده
Generator يحتوي منطق توليد الكود المحدد
AssetReader يقرأ ملفات الكود المصدري
AssetWriter يكتب الملفات المولدة
ملفات Part .g.dart / .freezed.dart

4. json_serializable

(1) التكوين والاستخدام

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: تكوين pubspec.yaml

YAML
dependencies:
  json_annotation: ^4.8.0

dev_dependencies:
  build_runner: ^2.4.0
  json_serializable: ^6.7.0

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: فئة نموذج أساسية

DART
// lib/src/models/order.dart
import 'package:json_annotation/json_annotation.dart';

part 'order.g.dart';

@JsonSerializable()
class Order {
  final String id;
  final double amount;
  final String status;
  final String? category;

  Order({
    required this.id,
    required this.amount,
    required this.status,
    this.category,
  });

  factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
  Map<String, dynamic> toJson() => _$OrderToJson(this);
}

// شغّل: dart run build_runner build
// يولد order.g.dart مع _$OrderFromJson و _$OrderToJson
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: تعيين حقل مخصص

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

part 'product.g.dart';

@JsonSerializable()
class Product {
  @JsonKey(name: 'product_id')
  final String id;

  @JsonKey(name: 'product_name')
  final String name;

  @JsonKey(name: 'unit_price')
  final double price;

  @JsonKey(defaultValue: 'General')
  final String category;

  @JsonKey(fromJson: _dateTimeFromEpoch, toJson: _dateTimeToEpoch)
  final DateTime createdAt;

  @JsonKey(ignore: true)
  final String? cachedData;

  Product({
    required this.id,
    required this.name,
    required this.price,
    this.category = 'General',
    required this.createdAt,
    this.cachedData,
  });

  factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
  Map<String, dynamic> toJson() => _$ProductToJson(this);
}

DateTime _dateTimeFromEpoch(int epoch) => DateTime.fromMillisecondsSinceEpoch(epoch);
int _dateTimeToEpoch(DateTime dt) => dt.millisecondsSinceEpoch;
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: تسلسل الكائنات المتداخلة

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

part 'customer.g.dart';

@JsonSerializable()
class Address {
  final String city;
  final String? state;
  final String country;

  Address({required this.city, this.state, required this.country});

  factory Address.fromJson(Map<String, dynamic> json) => _$AddressFromJson(json);
  Map<String, dynamic> toJson() => _$AddressToJson(this);
}

@JsonSerializable()
class Customer {
  final String name;
  final String email;
  final Address? address;

  Customer({required this.name, required this.email, this.address});

  factory Customer.fromJson(Map<String, dynamic> json) => _$CustomerFromJson(json);
  Map<String, dynamic> toJson() => _$CustomerToJson(this);
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
معامل @JsonKey المعنى مثال
name اسم المفتاح في JSON @JsonKey(name: 'product_id')
defaultValue القيمة الافتراضية عند الغياب @JsonKey(defaultValue: 'N/A')
fromJson دالة إلغاء تسلسل مخصصة @JsonKey(fromJson: _parse)
toJson دالة تسلسل مخصصة @JsonKey(toJson: _format)
ignore تجاهل هذا الحقل @JsonKey(ignore: true)

5. freezed

(1) توليد فئة بيانات غير قابلة للتغيير

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: استخدام freezed الأساسي

DART
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:json_annotation/json_annotation.dart';

part 'order_event.freezed.dart';
part 'order_event.g.dart';

@freezed
class OrderEvent with _$OrderEvent {
  const factory OrderEvent.created({
    required String orderId,
    required double amount,
    required DateTime timestamp,
  }) = OrderCreated;

  const factory OrderEvent.statusChanged({
    required String orderId,
    required String from,
    required String to,
  }) = OrderStatusChanged;

  const factory OrderEvent.cancelled({
    required String orderId,
    required String reason,
  }) = OrderCancelled;

  factory OrderEvent.fromJson(Map<String, dynamic> json) =>
      _$OrderEventFromJson(json);
}

// شغّل: dart run build_runner build
// يولد:
// - order_event.freezed.dart: copyWith, ==, hashCode, toString, pattern matching
// - order_event.g.dart: fromJson, toJson
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
ما يولده freezed الميزة
copyWith() نسخة غير قابلة للتغيير
== / hashCode مساواة القيمة
toString() إخراج منسق
when() استدعاء مطابقة الأنماط
maybeWhen() مطابقة أنماط اختيارية
fromJson/toJson تسلسل JSON

6. أوامر build_runner

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: الأوامر الشائعة

BASH
# بناء لمرة واحدة
dart run build_runner build

# وضع المراقبة - إعادة البناء عند تغيير الملفات
dart run build_runner watch

# تنظيف الملفات المولدة
dart run build_runner clean

# البناء مع حذف الملفات القديمة
dart run build_runner build --delete-conflicting-outputs

# المراقبة مع الحذف
dart run build_runner watch --delete-conflicting-outputs
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
الأمر الغرض مرحلة التطوير
build توليد لمرة واحدة الإصدار
watch توليد تلقائي عند التغييرات التطوير
clean إزالة الملفات المولدة إعادة تعيين
--delete-conflicting-outputs استبدال التعارضات تلقائياً تصحيح الأخطاء

7. آلية ملف Part

(1) part و part of

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: علاقة ملف Part

DART
// lib/models/order.dart (ملف المصدر)
import 'package:json_annotation/json_annotation.dart';

// إعلان ملفات part
part 'order.g.dart';        // مولد بواسطة json_serializable
part 'order.freezed.dart';  // مولد بواسطة freezed (إذا تم استخدامه)

@JsonSerializable()
class Order {
  final String id;
  final double amount;
  Order({required this.id, required this.amount});

  factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
  Map<String, dynamic> toJson() => _$OrderToJson(this);
}

// مولد: order.g.dart
// part of 'order.dart';
// Order _$OrderFromJson(Map<String, dynamic> json) => Order(...)
// Map<String, dynamic> _$OrderToJson(Order instance) => {...}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
التوجيه الموقع المعنى
part 'file.dart' ملف المصدر يعلن عن ملف part
part of 'file.dart' ملف مولد يشير إلى ملف المصدر الذي ينتمي إليه
.g.dart ملف مولد من json_serializable
.freezed.dart ملف مولد من freezed

8. مقدمة في Builders المخصصة

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: مفهوم Builder بسيط

DART
// مفهوم builder مخصص (مبسط)
// في مشروع حقيقي، سيكون في حزمة منفصلة

// تعليق توضيحي مخصص
class CsvMapping {
  final String columnName;
  final bool required;

  const CsvMapping({required this.columnName, this.required = true});
}

// نموذج يستخدم التعليق التوضيحي
class Order {
  @CsvMapping(columnName: 'order_id')
  final String id;

  @CsvMapping(columnName: 'total_amount', required: false)
  final double amount;

  Order({required this.id, required this.amount});
}

// ما سيولده builder مخصص:
// order.mapper.dart
// part of 'order.dart';
//
// Order OrderFromCsv(Map<String, String> row) => Order(
//   id: row['order_id'] ?? '',
//   amount: double.tryParse(row['total_amount'] ?? '0') ?? 0,
// );
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

9. سيناريو بوب: توليد كود التسلسل لخط أنابيب البيانات

▶ مثال

TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

: تعريف النموذج الكامل

DART
// lib/src/models/order.dart
import 'package:json_annotation/json_annotation.dart';

part 'order.g.dart';

@JsonSerializable(createToJson: true, createFactory: true)
class Order {
  @JsonKey(name: 'order_id')
  final String id;

  @JsonKey(name: 'total_amount')
  final double amount;

  @JsonKey(name: 'order_status', defaultValue: 'pending')
  final String status;

  @JsonKey(name: 'product_category', required: false)
  final String? category;

  @JsonKey(name: 'discount_rate', defaultValue: 0)
  final double discountRate;

  @JsonKey(name: 'created_at')
  final DateTime createdAt;

  Order({
    required this.id,
    required this.amount,
    this.status = 'pending',
    this.category,
    this.discountRate = 0,
    DateTime? createdAt,
  }) : createdAt = createdAt ?? DateTime.now();

  // مولد بواسطة build_runner
  factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
  Map<String, dynamic> toJson() => _$OrderToJson(this);

  // خصائص محسوبة (ليست في JSON)
  double get tax => amount * 0.08;
  double get total => amount * (1 - 0.08) * (1 - discountRate);
  String get formatAmount => '\$${amount.toStringAsFixed(2)} USD';
}

// بعد تشغيل: dart run build_runner build
// يتم توليد ملف order.g.dart مع:
// - _$OrderFromJson: تحليل JSON إلى Order
// - _$OrderToJson: تسلسل Order إلى JSON
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

10. المثال الكامل: تسلسل نموذج خط أنابيب البيانات

DART
// ============================================
// تسلسل نموذج خط أنابيب البيانات
// نماذج كاملة مع json_serializable
// ============================================

import 'package:json_annotation/json_annotation.dart';

part 'models.g.dart';

// نموذج الطلب
@JsonSerializable()
class Order {
  @JsonKey(name: 'order_id')
  final String id;

  @JsonKey(name: 'total_amount')
  final double amount;

  @JsonKey(defaultValue: 'pending')
  final String status;

  @JsonKey(name: 'category')
  final String? category;

  @JsonKey(name: 'discount_rate', defaultValue: 0.0)
  final double discountRate;

  @JsonKey(name: 'region', defaultValue: 'US')
  final String region;

  Order({
    required this.id,
    required this.amount,
    this.status = 'pending',
    this.category,
    this.discountRate = 0.0,
    this.region = 'US',
  });

  factory Order.fromJson(Map<String, dynamic> json) => _$OrderFromJson(json);
  Map<String, dynamic> toJson() => _$OrderToJson(this);

  double get effectiveAmount => amount * (1 - discountRate);
  double get tax => effectiveAmount * 0.08;
  double get total => effectiveAmount + tax;
  String get formatTotal => '\$${total.toStringAsFixed(2)} USD';
}

// نموذج التقرير
@JsonSerializable()
class Report {
  final String title;
  final int totalOrders;
  final int completedOrders;
  final double totalRevenue;
  final double totalTax;
  final Map<String, double> revenueByCategory;
  final DateTime generatedAt;

  Report({
    required this.title,
    required this.totalOrders,
    required this.completedOrders,
    required this.totalRevenue,
    required this.totalTax,
    required this.revenueByCategory,
    DateTime? generatedAt,
  }) : generatedAt = generatedAt ?? DateTime.now();

  factory Report.fromJson(Map<String, dynamic> json) => _$ReportFromJson(json);
  Map<String, dynamic> toJson() => _$ReportToJson(this);

  String get formatRevenue => '\$${totalRevenue.toStringAsFixed(2)} USD';
  double get averageOrderValue => completedOrders > 0 ? totalRevenue / completedOrders : 0;
}

// الاستخدام المحاكي (في الإنتاج، سيتم توليد .g.dart)
void main() {
  // محاكاة ما سيقوم به الكود المولد
  final json = {
    'order_id': 'ORD-001',
    'total_amount': 1500.0,
    'status': 'completed',
    'category': 'Electronics',
    'discount_rate': 0.1,
    'region': 'US',
  };

  // التحليل اليدوي (محاكاة _$OrderFromJson)
  final order = Order(
    id: json['order_id'] as String,
    amount: (json['total_amount'] as num).toDouble(),
    status: json['status'] as String? ?? 'pending',
    category: json['category'] as String?,
    discountRate: (json['discount_rate'] as num?)?.toDouble() ?? 0.0,
    region: json['region'] as String? ?? 'US',
  );

  print('=== DataPipeline Order ===');
  print('ID:       ${order.id}');
  print('Amount:   \$${order.amount.toStringAsFixed(2)} USD');
  print('Discount: ${(order.discountRate * 100).toStringAsFixed(0)}%');
  print('Tax:      \$${order.tax.toStringAsFixed(2)} USD');
  print('Total:    ${order.formatTotal}');
  print('Category: ${order.category ?? "N/A"}');
  print('Region:   ${order.region}');

  // محاكاة توليد التقرير
  final report = Report(
    title: 'Daily Analytics Report',
    totalOrders: 1000,
    completedOrders: 850,
    totalRevenue: 525000.0,
    totalTax: 42000.0,
    revenueByCategory: {
      'Electronics': 360000.0,
      'Clothing': 89000.0,
      'Books': 76000.0,
    },
  );

  print('\n=== Report ===');
  print('Title:    ${report.title}');
  print('Orders:   ${report.completedOrders}/${report.totalOrders}');
  print('Revenue:  ${report.formatRevenue}');
  print('Average:  \$${report.averageOrderValue.toStringAsFixed(2)} USD');
}
TEXT 📖 للعرض فقط
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.

الإخراج:

TEXT 📖 للعرض فقط
=== DataPipeline Order ===
ID:       ORD-001
Amount:   $1500.00 USD
Discount: 10%
Tax:      $108.00 USD
Total:    $1458.00 USD
Category: Electronics
Region:   US

=== Report ===
Title:    Daily Analytics Report
Orders:   850/1000
Revenue:  $525000.00 USD
Average:  $617.65 USD

❓ أسئلة شائعة

س: هل سيكون وضع build_runner watch بطيئاً؟ ج: يحتاج البناء الأولي إلى فحص جميع الملفات ويمكن أن يكون بطيئاً (10-30 ثانية). عمليات البناء التدريجية اللاحقة تعالج فقط الملفات المعدلة، وعادة ما تستغرق 1-3 ثوانٍ. للمشاريع الكبيرة، استخدم --build-filter لتوليد الملفات المحددة فقط.

س: هل هناك فرق في الأداء بين json_serializable و fromJson المكتوب يدوياً؟ ج: عملياً لا. الكود المولد له جودة مشابهة للكود المكتوب يدوياً، وفي بعض السيناريوهات قد يكون أفضل (باستخدام فحوصات نوع أكثر دقة).

س: هل يجب استخدام freezed و json_serializable معاً؟ ج: لا. يتعامل json_serializable مع التسلسل، بينما يتعامل freezed مع توليد الفئات غير القابلة للتغيير. يمكنك استخدام json_serializable وحده، أو دمج الاثنين.

س: هل يجب رفع الملفات المولدة إلى التحكم في الإصدارات؟ ج: لمشاريع التطبيقات، يُنصح بالرفع (لضمان أن CI لا يتطلب build_runner)؛ لمشاريع المكتبات، يُنصح بالرفع (بما أن النشر على pub.dev يتطلبها). بعض الفرق تختار .gitignore للملفات المولدة.

س: كيف أصحح الكود المولد؟ ج: افتح ملف .g.dart مباشرة لقراءته. الكود المولد هو كود Dart قياسي؛ يمكنك تعيين نقاط توقف، وإضافة عبارات print. المشاكل عادة ما تكون في تكوين التعليق التوضيحي، وليس في منطق التوليد.

س: ما العلاقة بين build_runner و source_gen؟ ج: source_gen هو تجريد أعلى مستوى لـ build_runner، يوفر API أبسط لكتابة Builders مخصصة. كل من json_serializable و freezed مبنيان على source_gen.

س: هل يمكن تخصيص الأنواع لـ fromJson/toJson في @JsonKey؟ ج: نعم. عرّف دالة على المستوى الأعلى أو طريقة ثابتة. يجب أن يطابق التوقيع T fromJson(Object? json) و Object toJson(T value). ثم ارجع إليها في @JsonKey.


📖 ملخص


📝 تمارين

  1. أساسي (صعوبة ⭐): أنشئ مشروع Dart، أضف تبعيات json_serializable و build_runner. أضف تعليق @JsonSerializable إلى فئة Product بسيطة (مع 3 حقول)، شغّل dart run build_runner build، وافحص ملف .g.dart المولد.
  2. متوسط (صعوبة ⭐⭐): كون json_serializable لنموذج متداخل (Order يحتوي على List<Product>، حيث Product يحتوي على enum Category). تعامل مع أسماء الحقول المخصصة والقيم الافتراضية. تحقق من صحة الذهاب والإياب لـ fromJson/toJson.
  3. تحدي (صعوبة ⭐⭐⭐): ادمج freezed و json_serializable لإنشاء نموذج OrderEvent بنمط الفئة المختومة لخط أنابيب البيانات (Created/StatusChanged/Cancelled). ولد فئات غير قابلة للتغيير + تسلسل JSON + copyWith. اكتب اختبارات للتحقق من الكود المولد.

← الدرس السابق | الدرس التالي →

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%