Dart: توليد الكود في Dart — build_runner والتسلسل
آخر تحديث: 2026-08-26
توليد الكود هو السلاح النهائي للقضاء على الكود النمطي — دع الآلات تكتب الكود، ودع البشر يكتبون المنطق.
1. ما ستتعلمه
- كيف يعمل build_runner: Builder / Generator / AssetReader
- المولدات الشائعة: json_serializable / freezed / dart_mappable
- آلية ملف Part: .g.dart / .freezed.dart
- مقدمة في تطوير Builders مخصصة
- سيناريو بوب: خط أنابيب البيانات يولد كود التسلسل تلقائياً باستخدام json_serializable
2. قصة مطور حقيقية
(1) نقطة الألم: 3 أيام أمضيت في تسلسل يدوي لـ 20 فئة نموذج
يحتوي خط أنابيب البيانات الخاص ببوب على 20 فئة نموذج بيانات، كل منها يتطلب طرق fromJson/toJson. كتابة كود التسلسل يدوياً لـ 20 فئة استغرقت 3 أيام، حدثت خلالها 4 أخطاء إملائية و 2 من أخطاء تحويل النوع. والأسوأ من ذلك، في كل مرة تتم إضافة حقل جديد، يجب تحديث 3 أماكن في الكود يدوياً (إعلان الحقل، fromJson، toJson). تحديث واحد مفقود تسبب في فشل تحليل 100,000 سجل.
(2) حل json_serializable
علّق على فئة النموذج بـ @JsonSerializable، و build_runner يولد تلقائياً fromJson/toJson. إضافة حقل جديد تتطلب فقط الإعلان + إعادة تشغيل البناء، دون مخاطر الإغفال.
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);
}
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
(3) الفوائد
- كود التسلسل لـ 20 فئة تم تخفيضه من 3 أيام إلى 30 دقيقة
- إضافة حقل جديد تتطلب تغيير مكان واحد فقط؛ المولد يحدث تلقائياً
- يمكن اكتشاف أخطاء تحويل النوع في وقت الترجمة
3. كيف يعمل build_runner
(1) خط أنابيب التوليد
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
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
| المكون | المسؤولية |
|---|---|
| Builder | يقرأ ملفات المصدر، يحدد ما يجب توليده |
| Generator | يحتوي منطق توليد الكود المحدد |
| AssetReader | يقرأ ملفات الكود المصدري |
| AssetWriter | يكتب الملفات المولدة |
| ملفات Part | .g.dart / .freezed.dart |
4. json_serializable
(1) التكوين والاستخدام
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: تكوين pubspec.yaml
dependencies:
json_annotation: ^4.8.0
dev_dependencies:
build_runner: ^2.4.0
json_serializable: ^6.7.0
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: فئة نموذج أساسية
// 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
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: تعيين حقل مخصص
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;
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: تسلسل الكائنات المتداخلة
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);
}
> **الإخراج:** شغّل في 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) توليد فئة بيانات غير قابلة للتغيير
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: استخدام freezed الأساسي
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
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
| ما يولده freezed | الميزة |
|---|---|
copyWith() |
نسخة غير قابلة للتغيير |
== / hashCode |
مساواة القيمة |
toString() |
إخراج منسق |
when() |
استدعاء مطابقة الأنماط |
maybeWhen() |
مطابقة أنماط اختيارية |
fromJson/toJson |
تسلسل JSON |
6. أوامر build_runner
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: الأوامر الشائعة
# بناء لمرة واحدة
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
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
| الأمر | الغرض | مرحلة التطوير |
|---|---|---|
build |
توليد لمرة واحدة | الإصدار |
watch |
توليد تلقائي عند التغييرات | التطوير |
clean |
إزالة الملفات المولدة | إعادة تعيين |
--delete-conflicting-outputs |
استبدال التعارضات تلقائياً | تصحيح الأخطاء |
7. آلية ملف Part
(1) part و part of
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: علاقة ملف Part
// 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) => {...}
> **الإخراج:** شغّل في 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 المخصصة
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: مفهوم Builder بسيط
// مفهوم 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,
// );
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
9. سيناريو بوب: توليد كود التسلسل لخط أنابيب البيانات
▶ مثال
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
: تعريف النموذج الكامل
// 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
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
10. المثال الكامل: تسلسل نموذج خط أنابيب البيانات
// ============================================
// تسلسل نموذج خط أنابيب البيانات
// نماذج كاملة مع 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');
}
> **الإخراج:** شغّل في DartPad محلي أو عبر `dart run`. جميع الأمثلة في دورة Dart مبنية على Dart 3.x / Flutter 3.x. قد تختلف النتائج قليلاً حسب إصدار SDK.
الإخراج:
=== 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.
📖 ملخص
build_runnerهو محرك التنفيذ لتوليد كود Dart: يقرأ التعليقات التوضيحية ← يولد الكودjson_serializableيولد تلقائياًfromJson/toJson؛@JsonKeyيخصص التعيينfreezedيولد فئات بيانات غير قابلة للتغيير:copyWith،==،hashCode،when- آلية ملف Part تربط الكود المولد بالكود المصدري
- يستخدم خط أنابيب البيانات
json_serializableللقضاء على التسلسل المكتوب يدوياً لـ 20 فئة نموذج
📝 تمارين
- أساسي (صعوبة ⭐): أنشئ مشروع Dart، أضف تبعيات
json_serializableوbuild_runner. أضف تعليق@JsonSerializableإلى فئةProductبسيطة (مع 3 حقول)، شغّلdart run build_runner build، وافحص ملف.g.dartالمولد. - متوسط (صعوبة ⭐⭐): كون
json_serializableلنموذج متداخل (Orderيحتوي علىList<Product>، حيثProductيحتوي على enumCategory). تعامل مع أسماء الحقول المخصصة والقيم الافتراضية. تحقق من صحة الذهاب والإياب لـfromJson/toJson. - تحدي (صعوبة ⭐⭐⭐): ادمج
freezedوjson_serializableلإنشاء نموذجOrderEventبنمط الفئة المختومة لخط أنابيب البيانات (Created/StatusChanged/Cancelled). ولد فئات غير قابلة للتغيير + تسلسل JSON +copyWith. اكتب اختبارات للتحقق من الكود المولد.