ميزات Dart المتقدمة

توليد الكود والتعليقات التوضيحية

50 دقيقة الدرس 15 من 16

مقدمة في التعليقات التوضيحية في Dart

التعليقات التوضيحية (تسمى أيضاً البيانات الوصفية) هي طريقة لإرفاق معلومات إضافية بكودك. لا تغير كيفية تشغيل كودك، لكنها توفر تلميحات للأدوات والأطر ومولدات الكود. لقد رأيت بالفعل التعليقات التوضيحية المدمجة مثل @override و @deprecated — في هذا الدرس، ستتعلم كيف تعمل تحت الغطاء وكيفية إنشاء تعليقاتك الخاصة.

التعليقات التوضيحية هي واحدة من أقوى ميزات Dart لتقليل الكود المتكرر. من خلال التعليق على فئة، يمكن لمولد الكود إنتاج تسلسل JSON وفئات بيانات غير قابلة للتغيير وطرق المساواة و copyWith وأكثر من ذلك بكثير تلقائياً.

ملاحظة: التعليقات التوضيحية في Dart هي ثوابت وقت الترجمة. إنها نسخ من فئات مُنشئاتها const. وقت تشغيل Dart لا يمتلك انعكاساً في الكود المُجمّع AOT (قبل وقت التشغيل)، لذا تُستهلك التعليقات التوضيحية بشكل أساسي بواسطة أدوات التحليل الثابت ومولدات الكود — وليس في وقت التشغيل.

التعليقات التوضيحية المدمجة

يوفر Dart ومكتباته الأساسية عدة تعليقات توضيحية مدمجة يجب أن تكون مألوفة لديك بالفعل. لنراجعها ونفهم متى نستخدم كل واحدة.

التعليقات التوضيحية المدمجة

import 'package:meta/meta.dart';

// @override — تشير إلى أن الطريقة تتجاوز طريقة الفئة الأب
class Animal {
  String speak() => '...';
}

class Dog extends Animal {
  @override
  String speak() => 'Woof!';
}

// @deprecated — تحدد عضواً كمهمل
class OldApi {
  @deprecated
  void oldMethod() => print('Use newMethod instead');

  // إهمال أكثر وصفية مع فئة Deprecated
  @Deprecated('Use processV2() instead. Will be removed in v3.0.0')
  void process() => print('Old processing');

  void processV2() => print('New processing');
}

// @pragma — تلميحات للمُجمّع (متقدم)
class HeavyComputation {
  @pragma('vm:prefer-inline')
  int add(int a, int b) => a + b;
}

// من package:meta — تعليقات توضيحية إضافية للتحليل
class MyWidget {
  @protected   // فقط الفئات الفرعية يجب أن تستخدم هذا
  void internalBuild() {}

  @mustCallSuper  // الفئات الفرعية يجب أن تستدعي super
  void dispose() {
    print('Cleaning up...');
  }

  @nonVirtual  // لا يمكن تجاوزها
  void coreLogic() {}

  @visibleForTesting  // عامة فقط للوصول من الاختبار
  void resetState() {}
}

// @immutable — الفئة وجميع حقولها يجب أن تكون final
@immutable
class Point {
  final double x;
  final double y;
  const Point(this.x, this.y);
}
نصيحة: أضف package:meta لتبعياتك للوصول إلى تعليقات توضيحية مثل @protected و @mustCallSuper و @immutable و @visibleForTesting. محلل Dart يفهم هذه التعليقات التوضيحية وسينتج تحذيرات عند انتهاكها.

إنشاء تعليقات توضيحية مخصصة

بما أن التعليقات التوضيحية هي مجرد نسخ ثابتة من الفئات، فإن إنشاء تعليقاتك الخاصة أمر مباشر. تحدد فئة مع مُنشئ const وتستخدمها مع البادئة @.

فئات تعليقات توضيحية مخصصة

// تعليقة توضيحية علامة بسيطة (بدون معاملات)
class Todo {
  final String message;
  final String? assignee;

  const Todo(this.message, {this.assignee});
}

// تعليقة توضيحية للمسار لإطار عمل ويب
class Route {
  final String path;
  final String method;

  const Route(this.path, {this.method = 'GET'});
}

// تعليقة توضيحية للتحقق
class Range {
  final num min;
  final num max;

  const Range({required this.min, required this.max});
}

// تعليقة توضيحية للتسلسل
class JsonField {
  final String? name;
  final bool ignore;

  const JsonField({this.name, this.ignore = false});
}

// استخدام التعليقات التوضيحية المخصصة
@Todo('Add caching', assignee: 'Alice')
class UserService {
  @Route('/users', method: 'GET')
  Future<List<String>> getUsers() async => ['Alice', 'Bob'];

  @Route('/users', method: 'POST')
  Future<void> createUser(String name) async {
    print('Creating $name');
  }
}

class Product {
  @JsonField(name: 'product_name')
  final String name;

  @JsonField()
  final double price;

  @JsonField(ignore: true)
  final String internalId;

  @Range(min: 0, max: 10000)
  final int quantity;

  Product(this.name, this.price, this.internalId, this.quantity);
}
ملاحظة: التعليقات التوضيحية المخصصة بحد ذاتها لا تفعل شيئاً في وقت التشغيل — إنها بيانات وصفية خاملة. تصبح قوية عندما يقرأها مولد كود أو أداة تحليل وينتج كوداً أو تحذيرات. في الأقسام التالية، سنرى كيف يقرأ build_runner و source_gen التعليقات التوضيحية لتوليد الكود.

مقدمة في build_runner وتوليد الكود

توليد الكود في Dart مدعوم بـ build_runner — نظام بناء يشغّل المولدات لإنتاج ملفات .g.dart من كودك المصدري. المولدات تقرأ تعليقاتك التوضيحية وتنتج كوداً متكرراً تلقائياً.

إعداد build_runner

# pubspec.yaml
name: my_app
environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  json_annotation: ^4.8.0

dev_dependencies:
  build_runner: ^2.4.0
  json_serializable: ^6.7.0

# أوامر الطرفية:

# تشغيل البناء مرة واحدة (يولّد ملفات .g.dart)
dart run build_runner build

# وضع المراقبة (إعادة التوليد تلقائياً عند تغيير الملفات)
dart run build_runner watch

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

# إعادة بناء قسرية (حذف المخرجات القديمة أولاً)
dart run build_runner build --delete-conflicting-outputs

كيف يعمل توليد الكود

يتبع خط أنابيب توليد الكود تدفقاً واضحاً:

  1. تكتب فئة Dart مع تعليقات توضيحية (مثل @JsonSerializable())
  2. تضيف توجيه part يشير للملف المُولّد (مثل part 'user.g.dart';)
  3. تشغّل dart run build_runner build
  4. المولد يقرأ تعليقاتك التوضيحية وينتج ملف .g.dart
  5. يمكن لكودك الآن استخدام الطرق المُولّدة

json_serializable: تسلسل JSON التلقائي

حزمة json_serializable هي أكثر مولدات الكود استخداماً في نظام Dart البيئي. تقرأ تعليقات @JsonSerializable() التوضيحية وتولّد طرق fromJson و toJson تلقائياً.

استخدام json_serializable الأساسي

import 'package:json_annotation/json_annotation.dart';

// هذا يخبر Dart أن 'user.g.dart' جزء من هذه المكتبة
part 'user.g.dart';

@JsonSerializable()
class User {
  final String name;
  final String email;
  final int age;

  @JsonKey(name: 'is_active')  // ربط بمفتاح JSON مختلف
  final bool isActive;

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

  User({
    required this.name,
    required this.email,
    required this.age,
    required this.isActive,
    required this.createdAt,
  });

  // هذه الطرق تفوّض للكود المُولّد
  factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
  Map<String, dynamic> toJson() => _$UserToJson(this);
}

ميزات json_serializable المتقدمة

الكائنات المتداخلة والتعدادات والمحولات المخصصة

import 'package:json_annotation/json_annotation.dart';

part 'models.g.dart';

// تسلسل التعدادات
enum UserRole {
  @JsonValue('admin')
  admin,
  @JsonValue('editor')
  editor,
  @JsonValue('viewer')
  viewer,
}

// محوّل مخصص للأنواع المعقدة
class TimestampConverter implements JsonConverter<DateTime, int> {
  const TimestampConverter();

  @override
  DateTime fromJson(int timestamp) =>
      DateTime.fromMillisecondsSinceEpoch(timestamp);

  @override
  int toJson(DateTime date) => date.millisecondsSinceEpoch;
}

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

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

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

@JsonSerializable(
  explicitToJson: true,       // الكائنات المتداخلة تستدعي toJson() أيضاً
  fieldRename: FieldRename.snake, // تحويل تلقائي camelCase -> snake_case
  includeIfNull: false,       // تخطي الحقول الفارغة في مخرجات JSON
)
class UserProfile {
  final String firstName;
  final String lastName;
  final UserRole role;
  final Address address;             // كائن متداخل
  final List<String> skills;         // قائمة

  @TimestampConverter()               // محوّل مخصص
  final DateTime lastLogin;

  @JsonKey(includeFromJson: false, includeToJson: false)
  final String? temporaryToken;       // مُستثنى من JSON

  @JsonKey(defaultValue: 0)
  final int loginCount;               // قيمة افتراضية إذا مفقود

  UserProfile({
    required this.firstName,
    required this.lastName,
    required this.role,
    required this.address,
    required this.skills,
    required this.lastLogin,
    this.temporaryToken,
    this.loginCount = 0,
  });

  factory UserProfile.fromJson(Map<String, dynamic> json) =>
      _$UserProfileFromJson(json);
  Map<String, dynamic> toJson() => _$UserProfileToJson(this);
}
تحذير: دائماً عيّن explicitToJson: true على الفئات ذات الكائنات المتداخلة. بدونها، تُسلسل الكائنات المتداخلة باستخدام toString() الافتراضية، والتي تنتج Instance of 'Address' بدلاً من JSON الفعلي. هذا خطأ شائع جداً.

freezed: فئات البيانات غير القابلة للتغيير

حزمة freezed تتجاوز تسلسل JSON. تولّد فئات بيانات غير قابلة للتغيير مع copyWith و == و hashCode و toString ودعم مطابقة الأنماط وأنواع الاتحاد — كلها من فئة واحدة مُعلّقة.

إعداد freezed

# pubspec.yaml
dependencies:
  freezed_annotation: ^2.4.0
  json_annotation: ^4.8.0

dev_dependencies:
  build_runner: ^2.4.0
  freezed: ^2.4.0
  json_serializable: ^6.7.0

فئة بيانات غير قابلة للتغيير مع freezed

import 'package:freezed_annotation/freezed_annotation.dart';

part 'user.freezed.dart';
part 'user.g.dart';

@freezed
class User with _$User {
  const factory User({
    required String name,
    required String email,
    required int age,
    @Default(true) bool isActive,
    List<String>? tags,
  }) = _User;

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

// بعد توليد الكود، تحصل على:
void main() {
  // إنشاء نسخة
  final user = User(name: 'Alice', email: 'alice@test.com', age: 30);

  // copyWith — إنشاء نسخة معدّلة
  final updated = user.copyWith(name: 'Bob', age: 25);
  print(updated); // User(name: Bob, email: alice@test.com, age: 25, ...)

  // مساواة عميقة (قائمة على القيمة وليس المرجع)
  final user2 = User(name: 'Alice', email: 'alice@test.com', age: 30);
  print(user == user2); // true (نفس القيم)

  // toString — مخرجات قابلة للقراءة
  print(user); // User(name: Alice, email: alice@test.com, age: 30, ...)

  // تسلسل JSON
  final json = user.toJson();
  final restored = User.fromJson(json);
  print(user == restored); // true
}

أنواع الاتحاد مع freezed

واحدة من أقوى ميزات freezed هي أنواع الاتحاد (الفئات المختومة). تتيح لك تحديد نوع يمكن أن يكون أحد عدة متغيرات، مع مطابقة أنماط شاملة.

أنواع الاتحاد (الفئات المختومة) مع freezed

import 'package:freezed_annotation/freezed_annotation.dart';

part 'result.freezed.dart';

@freezed
sealed class Result<T> with _$Result<T> {
  const factory Result.success(T data) = Success<T>;
  const factory Result.failure(String message, {int? code}) = Failure<T>;
  const factory Result.loading() = Loading<T>;
}

// الاستخدام مع مطابقة الأنماط
void handleResult(Result<String> result) {
  // شاملة — المُجمّع يضمن معالجة جميع الحالات
  switch (result) {
    case Success(:final data):
      print('Got data: $data');
    case Failure(:final message, :final code):
      print('Error ($code): $message');
    case Loading():
      print('Loading...');
  }
}

// بديل: استخدام طرق when/map المُولّدة بواسطة freezed
void handleResult2(Result<String> result) {
  final message = result.when(
    success: (data) => 'Success: $data',
    failure: (message, code) => 'Error: $message',
    loading: () => 'Loading...',
  );
  print(message);
}

void main() {
  final results = <Result<String>>[
    Result.success('Hello'),
    Result.failure('Not found', code: 404),
    Result.loading(),
  ];

  for (final r in results) {
    handleResult(r);
  }
}
نصيحة: استخدم أنواع اتحاد freezed لنمذجة الحالات في تطبيقك (تحميل/نجاح/خطأ) واستجابات الشبكة وأحداث التنقل أو أي سيناريو يمكن أن تكون فيه القيمة أحد عدة أنواع مميزة. مطابقة الأنماط الشاملة تضمن أنك لن تنسى معالجة حالة أبداً.

إنشاء مولدات كود مخصصة

فهم كيفية إنشاء مولد كود مخصص يساعدك في استيعاب كيف تعمل حزم مثل json_serializable و freezed داخلياً. بينما نادراً ما تحتاج لبناء واحد من الصفر، معرفة العملية قيّمة.

هيكل المولد المخصص

// الخطوة 1: تحديد تعليقتك التوضيحية (في حزمة منفصلة)
// ====== my_annotation/lib/my_annotation.dart ======
class AutoToString {
  final bool includePrivate;
  const AutoToString({this.includePrivate = false});
}

// الخطوة 2: إنشاء المولد (في حزمة منفصلة)
// ====== my_generator/lib/src/to_string_generator.dart ======
import 'package:analyzer/dart/element/element.dart';
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'package:my_annotation/my_annotation.dart';

class AutoToStringGenerator extends GeneratorForAnnotation<AutoToString> {
  @override
  String generateForAnnotatedElement(
    Element element,
    ConstantReader annotation,
    BuildStep buildStep,
  ) {
    if (element is! ClassElement) {
      throw InvalidGenerationSourceError(
        '@AutoToString can only be applied to classes.',
        element: element,
      );
    }

    final className = element.name;
    final includePrivate = annotation.read('includePrivate').boolValue;

    final fields = element.fields.where((f) {
      if (f.isStatic) return false;
      if (!includePrivate && f.name.startsWith('_')) return false;
      return true;
    });

    final fieldStrings = fields.map((f) =>
        '${f.name}: \${${f.name}}').join(', ');

    return '''
extension ${className}ToString on $className {
  String toDebugString() => '$className($fieldStrings)';
}
''';
  }
}

// الخطوة 3: إنشاء باني (يربط المولد بـ build_runner)
// ====== my_generator/lib/builder.dart ======
import 'package:build/build.dart';
import 'package:source_gen/source_gen.dart';
import 'src/to_string_generator.dart';

Builder autoToStringBuilder(BuilderOptions options) =>
    SharedPartBuilder([AutoToStringGenerator()], 'auto_to_string');

// الخطوة 4: التكوين في build.yaml
// الخطوة 5: الاستخدام في مشروعك
// ====== my_app/lib/models/product.dart ======
import 'package:my_annotation/my_annotation.dart';

part 'product.g.dart';

@AutoToString()
class Product {
  final String name;
  final double price;
  final int quantity;

  Product(this.name, this.price, this.quantity);
}

// بعد تشغيل build_runner، يحتوي product.g.dart على:
// extension ProductToString on Product {
//   String toDebugString() =>
//       'Product(name: $name, price: $price, quantity: $quantity)';
// }
ملاحظة: إنشاء مولد كود يتطلب ثلاث حزم: (1) حزمة التعليقات التوضيحية (يستهلكها المستخدمون)، (2) حزمة المولد (يستهلكها build_runner)، و(3) حزمة تطبيق المستخدم. هذا الفصل يمنع تبعيات المحلل/البناء الثقيلة من التضمين في كود وقت التشغيل.

سير عمل عملي: مشروع json_serializable كامل

لنمشِ عبر مثال حقيقي كامل لإعداد واستخدام json_serializable في مشروع مع فئات نموذج متعددة.

إعداد المشروع الكامل

// 1. pubspec.yaml
// name: blog_api
// dependencies:
//   json_annotation: ^4.8.0
//   http: ^1.1.0
// dev_dependencies:
//   build_runner: ^2.4.0
//   json_serializable: ^6.7.0

// 2. lib/models/post.dart
import 'package:json_annotation/json_annotation.dart';
import 'author.dart';
import 'comment.dart';

part 'post.g.dart';

@JsonSerializable(explicitToJson: true)
class Post {
  final int id;
  final String title;
  final String body;
  final Author author;
  final List<Comment> comments;

  @JsonKey(name: 'published_at')
  final DateTime publishedAt;

  @JsonKey(name: 'is_featured', defaultValue: false)
  final bool isFeatured;

  Post({
    required this.id,
    required this.title,
    required this.body,
    required this.author,
    required this.comments,
    required this.publishedAt,
    this.isFeatured = false,
  });

  factory Post.fromJson(Map<String, dynamic> json) => _$PostFromJson(json);
  Map<String, dynamic> toJson() => _$PostToJson(this);
}

// 3-4. فئات Author و Comment بنفس النمط...

// 5. شغّل: dart run build_runner build

// 6. الاستخدام في كودك
import 'dart:convert';

void main() {
  final jsonString = '''
  {
    "id": 1,
    "title": "Getting Started with Dart",
    "body": "Dart is a great language...",
    "author": {"id": 10, "name": "Alice", "avatar_url": null},
    "comments": [
      {"id": 1, "text": "Great post!", "user_name": "Bob",
       "created_at": "2024-01-15T10:30:00Z"}
    ],
    "published_at": "2024-01-15T09:00:00Z",
    "is_featured": true
  }
  ''';

  final post = Post.fromJson(jsonDecode(jsonString));
  print('${post.title} by ${post.author.name}');
  print('Comments: ${post.comments.length}');
  print('Featured: ${post.isFeatured}');

  // رحلة ذهاب وإياب: التحويل مرة أخرى إلى JSON
  final json = post.toJson();
  print(const JsonEncoder.withIndent('  ').convert(json));
}

أفضل ممارسات توليد الكود

أفضل ممارسات توليد الكود

// 1. دائماً أودع الملفات المُولّدة (.g.dart, .freezed.dart)
//    في التحكم بالإصدارات. هذا يضمن عمل CI/CD بدون
//    تشغيل build_runner.

// 2. أضف build.yaml للتكوين على مستوى المشروع

// 3. استخدم وضع المراقبة أثناء التطوير
// dart run build_runner watch

// 4. أنشئ ملف برميل للنماذج

// 5. تعامل مع القيم الفارغة والافتراضية بشكل صحيح
@JsonSerializable()
class Settings {
  @JsonKey(defaultValue: 'en')
  final String language;

  @JsonKey(defaultValue: false)
  final bool darkMode;

  final String? optionalField;  // قابل للإلغاء = اختياري طبيعياً

  Settings({
    this.language = 'en',
    this.darkMode = false,
    this.optionalField,
  });

  factory Settings.fromJson(Map<String, dynamic> json) =>
      _$SettingsFromJson(json);
  Map<String, dynamic> toJson() => _$SettingsToJson(this);
}

// 6. استخدم أغلفة عامة لاستجابات API
@JsonSerializable(genericArgumentFactories: true)
class PaginatedResponse<T> {
  final List<T> data;
  final int total;
  final int page;

  @JsonKey(name: 'per_page')
  final int perPage;

  PaginatedResponse({
    required this.data,
    required this.total,
    required this.page,
    required this.perPage,
  });

  factory PaginatedResponse.fromJson(
    Map<String, dynamic> json,
    T Function(Object? json) fromJsonT,
  ) => _$PaginatedResponseFromJson(json, fromJsonT);

  Map<String, dynamic> toJson(
    Object? Function(T value) toJsonT,
  ) => _$PaginatedResponseToJson(this, toJsonT);
}
نصيحة: اضبط الإعدادات الافتراضية على مستوى المشروع في build.yaml لتجنب تكرار خيارات مثل explicitToJson: true على كل فئة. هذا يبقي فئات نماذجك نظيفة ومتسقة.

الملخص

في هذا الدرس، تعلمت كيفية إزالة الكود المتكرر من خلال التعليقات التوضيحية وتوليد الكود:

  • التعليقات التوضيحية المدمجة@override و @deprecated و @pragma وتعليقات package:meta للتحليل الثابت
  • التعليقات التوضيحية المخصصة — إنشاء فئات const لإرفاق بيانات وصفية بعناصر الكود
  • build_runner — نظام البناء الذي يدعم توليد كود Dart مع أوامر build و watch و clean
  • json_serializable — تسلسل JSON التلقائي مع @JsonSerializable و @JsonKey والمحولات المخصصة ودعم التعدادات
  • freezed — فئات بيانات غير قابلة للتغيير مع copyWith ومساواة القيمة و toString وأنواع اتحاد لمطابقة أنماط شاملة
  • المولدات المخصصة — معمارية الحزم الثلاث (تعليقة توضيحية + مولد + مستهلك) باستخدام source_gen
  • أفضل الممارسات — إيداع الملفات المُولّدة والتكوين على مستوى المشروع وأغلفة الاستجابة العامة