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

حزم ومكتبات Dart

45 دقيقة الدرس 14 من 16

مقدمة في حزم Dart

الحزم هي الوحدة الأساسية لمشاركة وإعادة استخدام الكود في Dart. سواء كنت تضيف تبعية من pub.dev (سجل الحزم الرسمي لـ Dart) أو تنشئ مكتبتك القابلة لإعادة الاستخدام، فإن فهم نظام الحزم ضروري لبناء تطبيقات Dart و Flutter قابلة للصيانة.

كل مشروع Dart هو نفسه حزمة. ملف pubspec.yaml في جذر المشروع يحدد اسم الحزمة وإصدارها وتبعياتها وبياناتها الوصفية. عندما تشغّل dart pub get (أو flutter pub get)، يقوم Dart بتنزيل وحل جميع التبعيات المدرجة في هذا الملف.

ملاحظة: مدير حزم Dart يسمى pub. تتفاعل معه من خلال أوامر dart pub (أو flutter pub في مشاريع Flutter). المستودع المركزي لحزم Dart هو pub.dev.

ملف pubspec.yaml

ملف pubspec.yaml هو قلب كل حزمة Dart. يحدد كل شيء عن مشروعك: هويته وتبعياته وتكوين البناء.

مثال كامل لـ pubspec.yaml

name: my_awesome_app
description: A command-line application for data processing.
version: 1.2.0
homepage: https://github.com/myuser/my_awesome_app

environment:
  sdk: '>=3.0.0 <4.0.0'

dependencies:
  http: ^1.1.0
  path: ^1.8.0
  args: ^2.4.0
  json_annotation: ^4.8.0

dev_dependencies:
  test: ^1.24.0
  lints: ^3.0.0
  build_runner: ^2.4.0
  json_serializable: ^6.7.0

executables:
  myapp: main

شرح الحقول الرئيسية

  • name — اسم الحزمة (أحرف صغيرة مع شرطات سفلية، يجب أن يكون فريداً على pub.dev إذا نُشر)
  • description — وصف مختصر (مطلوب للنشر)
  • version — الإصدار الدلالي: MAJOR.MINOR.PATCH
  • environment — قيود إصدار Dart SDK
  • dependencies — الحزم التي يحتاجها كودك في وقت التشغيل
  • dev_dependencies — الحزم المطلوبة فقط أثناء التطوير (الاختبار، توليد الكود، التحليل)
  • executables — يربط أسماء الملفات التنفيذية بملفات نقاط الدخول في bin/

قيود إصدار التبعيات

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

صيغة قيود الإصدار

# صيغة القيراط (موصى بها) — تسمح بتحديثات فرعية وتصحيحية
http: ^1.1.0        # تعادل: >=1.1.0 <2.0.0

# إصدار محدد (نادراً ما يُحتاج)
http: 1.1.0

# قيد نطاق
http: '>=1.0.0 <2.0.0'

# أي إصدار (خطير — تجنبه في الحزم المنشورة)
http: any

# تبعية Git (مفيدة للحزم غير المنشورة)
my_package:
  git:
    url: https://github.com/user/repo.git
    ref: main
    path: packages/my_package

# تبعية مسار (التطوير المحلي)
my_shared_lib:
  path: ../my_shared_lib

# مستضافة على خادم pub مخصص
my_internal_pkg:
  hosted:
    name: my_internal_pkg
    url: https://pub.mycompany.com
  version: ^1.0.0
نصيحة: دائماً استخدم صيغة القيراط (^) للحزم المنشورة. تسمح بالتحديثات المتوافقة (إصدارات التصحيح والفرعية) بينما تمنع التغييرات الكاسرة (زيادات الإصدار الرئيسي). هذا يتبع الممارسة الموصى بها من فريق Dart.

البحث عن الحزم وإضافتها

يستضيف سجل pub.dev آلاف الحزم المجتمعية والرسمية. إليك كيفية البحث عنها وتقييمها وإضافتها لمشروعك.

العمل مع pub.dev

# البحث عن الحزم من سطر الأوامر
dart pub search http

# إضافة تبعية (يحدّث pubspec.yaml تلقائياً)
dart pub add http
dart pub add path
dart pub add test --dev  # إضافة كتبعية تطوير

# إزالة تبعية
dart pub remove http

# الحصول على جميع التبعيات (تنزيل وحل)
dart pub get

# ترقية التبعيات لأحدث إصدارات متوافقة
dart pub upgrade

# عرض شجرة التبعيات
dart pub deps

# التحقق من الحزم القديمة
dart pub outdated

# التحقق من جاهزية حزمتك للنشر
dart pub publish --dry-run
تحذير: قبل إضافة حزمة، تحقق من صفحتها على pub.dev من: (1) نقاط الإعجابات ونقاط pub، (2) مقياس الشعبية، (3) مدى حداثة التحديث، و(4) ما إذا كانت تدعم المنصات المستهدفة (أصلي، ويب، Flutter). حزمة بنقاط منخفضة أو بدون تحديثات حديثة قد تكون مهجورة أو سيئة الصيانة.

إنشاء المكتبات مع library و export و part

في Dart، كل ملف .dart هو مكتبة. يمكنك تنظيم كودك في ملفات متعددة والتحكم فيما هو مرئي للمستهلكين باستخدام export و show و hide و part.

هيكل المكتبة الأساسي

أبسط طريقة لإنشاء مكتبة هي استخدام توجيهات export في ملف برميل واحد يعيد تصدير واجهتك العامة.

مكتبة مع Export (نمط ملف البرميل)

// ====== lib/my_library.dart (ملف البرميل) ======
/// نقطة الدخول الرئيسية لحزمة my_library.
library my_library;

export 'src/models/user.dart';
export 'src/models/product.dart';
export 'src/services/api_client.dart';
export 'src/utils/validators.dart';
// ملفات التنفيذ الخاصة لا يتم تصديرها

// ====== lib/src/models/user.dart ======
class User {
  final String name;
  final String email;

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

  @override
  String toString() => 'User($name, $email)';
}

// ====== lib/src/models/product.dart ======
class Product {
  final String title;
  final double price;

  Product({required this.title, required this.price});
}

// ====== lib/src/services/api_client.dart ======
class ApiClient {
  final String baseUrl;

  ApiClient(this.baseUrl);

  Future<String> get(String endpoint) async {
    // تنفيذ طلب HTTP
    return '{"status": "ok"}';
  }
}

// ====== كود المستهلك ======
// import 'package:my_library/my_library.dart';
// الآن User و Product و ApiClient كلها متاحة
ملاحظة: الملفات داخل lib/src/ تُعتبر خاصة بالحزمة. الحزم الأخرى لا يمكنها استيرادها مباشرة — يمكنها فقط الوصول لما تصدّره صراحة من ملف البرميل. هذا يمنحك تحكماً كاملاً في واجهتك العامة.

استخدام show و hide

عند استيراد مكتبة، يمكنك التحكم في الأسماء المُدخلة للنطاق باستخدام show و hide.

الاستيراد الانتقائي مع show و hide

// استيراد فئات محددة فقط
import 'package:my_library/my_library.dart' show User, Product;

// استيراد كل شيء ماعدا فئات محددة
import 'package:my_library/my_library.dart' hide ApiClient;

// استخدام بادئة لتجنب تضارب الأسماء
import 'package:my_library/my_library.dart' as mylib;

void main() {
  // مع show — فقط User و Product متاحتان
  var user = User(name: 'Alice', email: 'alice@test.com');

  // مع البادئة — كل شيء متاح تحت البادئة
  var client = mylib.ApiClient('https://api.example.com');
}

// يمكنك أيضاً استخدام show/hide في التصديرات
// ====== lib/my_library.dart ======
export 'src/models/user.dart' show User;
export 'src/services/api_client.dart' hide InternalHelper;

استخدام part و part of

توجيه part يقسم مكتبة واحدة عبر ملفات متعددة. جميع الأجزاء تتشارك نفس فضاء الأسماء ويمكنها الوصول لأعضاء بعضها الخاصة (الأسماء التي تبدأ بـ _).

تقسيم مكتبة مع part

// ====== lib/calculator.dart (ملف المكتبة الرئيسي) ======
library calculator;

part 'src/basic_operations.dart';
part 'src/advanced_operations.dart';

class Calculator {
  double _memory = 0;

  // يمكن استخدام دوال من الأجزاء
  double add(double a, double b) => _add(a, b);
  double subtract(double a, double b) => _subtract(a, b);
  double power(double base, int exp) => _power(base, exp);

  void store(double value) => _memory = value;
  double recall() => _memory;
}

// ====== lib/src/basic_operations.dart ======
part of '../calculator.dart';

// هذه الدوال يمكنها الوصول لأعضاء المكتبة الخاصة
double _add(double a, double b) => a + b;
double _subtract(double a, double b) => a - b;
double _multiply(double a, double b) => a * b;
double _divide(double a, double b) {
  if (b == 0) throw ArgumentError('Cannot divide by zero');
  return a / b;
}

// ====== lib/src/advanced_operations.dart ======
part of '../calculator.dart';

double _power(double base, int exponent) {
  double result = 1;
  for (var i = 0; i < exponent; i++) {
    result = _multiply(result, base); // يمكن الاستدعاء من أجزاء أخرى
  }
  return result;
}
تحذير: توجيه part/part of يُعتبر قديماً في Dart الحديث. يوصي فريق Dart باستخدام export والاستيرادات العادية بدلاً من ذلك. استخدم part فقط عندما تحتاج حقاً لملفات متعددة تتشارك النطاق الخاص — مثلاً، في سيناريوهات توليد الكود مثل json_serializable حيث ملف .g.dart المُولّد هو part من ملفك المصدري.

الاستيراد المؤجل (الكسول)

الاستيرادات المؤجلة تسمح لك بتحميل مكتبة عند الطلب بدلاً من عند بدء تشغيل التطبيق. هذا مفيد بشكل خاص لتقليل وقت التحميل الأولي في تطبيقات الويب.

الاستيرادات المؤجلة

import 'package:heavy_library/heavy_library.dart' deferred as heavy;

Future<void> main() async {
  print('App started. Heavy library not loaded yet.');

  // تحميل المكتبة عندما تحتاجها فعلاً
  await heavy.loadLibrary();

  // الآن يمكنك استخدام فئاتها ودوالها
  var processor = heavy.DataProcessor();
  var result = processor.process([1, 2, 3]);
  print('Result: $result');
}

// نمط شائع: لف التحميل المؤجل في دالة
Future<void> processData(List<int> data) async {
  await heavy.loadLibrary();
  var processor = heavy.DataProcessor();
  print(processor.process(data));
}

// التحقق من تحميل المكتبة (استدعاء loadLibrary عدة مرات آمن)
Future<void> ensureLoaded() async {
  // loadLibrary() آمنة للاستدعاء عدة مرات
  // تعود فوراً إذا كانت محمّلة بالفعل
  await heavy.loadLibrary();
}
ملاحظة: الاستيرادات المؤجلة لها أكبر تأثير في تطبيقات Dart للويب المُجمّعة مع dart2js، حيث تمكّن تقسيم الكود. في تطبيقات Dart الأصلية (CLI، الخادم، Flutter)، المكتبة مُدمجة في الملف الثنائي بغض النظر، لذا فائدة الأداء ضئيلة. ومع ذلك، يمكن أن تكون الاستيرادات المؤجلة مفيدة لتنظيم ترتيب التهيئة.

إنشاء حزمتك الخاصة

إنشاء حزمة قابلة لإعادة الاستخدام يتبع هيكل مجلدات قياسي ومجموعة من الاتفاقيات. لنبنِ حزمة أدوات مساعدة كاملة من الصفر.

هيكل مجلدات الحزمة

my_utils/
  lib/
    my_utils.dart           # ملف البرميل الرئيسي (الواجهة العامة)
    src/
      string_utils.dart     # ملفات التنفيذ
      date_utils.dart
      validators.dart
  test/
    string_utils_test.dart  # الاختبارات تعكس هيكل lib/src/
    date_utils_test.dart
    validators_test.dart
  example/
    example.dart            # أمثلة الاستخدام
  pubspec.yaml
  README.md
  CHANGELOG.md
  LICENSE
  analysis_options.yaml

بناء حزمة أدوات مساعدة

// ====== pubspec.yaml ======
// name: my_utils
// version: 1.0.0
// description: A collection of handy utility functions.
// environment:
//   sdk: '>=3.0.0 <4.0.0'

// ====== lib/my_utils.dart (ملف البرميل) ======
library my_utils;

export 'src/string_utils.dart';
export 'src/date_utils.dart';
export 'src/validators.dart';

// ====== lib/src/string_utils.dart ======
/// دوال مساعدة لمعالجة النصوص.
extension StringUtils on String {
  /// تكبير الحرف الأول من النص.
  String get capitalized {
    if (isEmpty) return this;
    return '${this[0].toUpperCase()}${substring(1)}';
  }

  /// التحويل لحالة العنوان (تكبير كل كلمة).
  String get titleCase {
    return split(' ').map((w) => w.capitalized).join(' ');
  }

  /// القص لطول أقصى مع علامة حذف.
  String truncate(int maxLength, {String ellipsis = '...'}) {
    if (length <= maxLength) return this;
    return '${substring(0, maxLength - ellipsis.length)}$ellipsis';
  }

  /// التحويل لتنسيق slug (صديق لعنوان URL).
  String get slugified {
    return toLowerCase()
        .replaceAll(RegExp(r'[^a-z0-9\s-]'), '')
        .replaceAll(RegExp(r'[\s-]+'), '-')
        .replaceAll(RegExp(r'^-|-$'), '');
  }
}

// ====== lib/src/date_utils.dart ======
/// دوال مساعدة لمعالجة التواريخ.
extension DateUtils on DateTime {
  /// التحقق إذا كان هذا التاريخ هو اليوم.
  bool get isToday {
    final now = DateTime.now();
    return year == now.year && month == now.month && day == now.day;
  }

  /// تنسيق كوقت نسبي (مثل "منذ ساعتين").
  String get timeAgo {
    final diff = DateTime.now().difference(this);
    if (diff.inDays > 365) return '${diff.inDays ~/ 365}y ago';
    if (diff.inDays > 30) return '${diff.inDays ~/ 30}mo ago';
    if (diff.inDays > 0) return '${diff.inDays}d ago';
    if (diff.inHours > 0) return '${diff.inHours}h ago';
    if (diff.inMinutes > 0) return '${diff.inMinutes}m ago';
    return 'just now';
  }

  /// الحصول على بداية اليوم (منتصف الليل).
  DateTime get startOfDay => DateTime(year, month, day);

  /// الحصول على نهاية اليوم.
  DateTime get endOfDay => DateTime(year, month, day, 23, 59, 59, 999);
}

// ====== lib/src/validators.dart ======
/// مدققات الإدخال الشائعة.
class Validators {
  static bool isEmail(String value) {
    return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(value);
  }

  static bool isUrl(String value) {
    return Uri.tryParse(value)?.hasAbsolutePath ?? false;
  }

  static bool isStrongPassword(String value) {
    if (value.length < 8) return false;
    if (!value.contains(RegExp(r'[A-Z]'))) return false;
    if (!value.contains(RegExp(r'[a-z]'))) return false;
    if (!value.contains(RegExp(r'[0-9]'))) return false;
    if (!value.contains(RegExp(r'[!@#\$%^&*]'))) return false;
    return true;
  }
}

مثال عملي: حزمة عميل API قابلة لإعادة الاستخدام

لنبنِ مثالاً أكثر اكتمالاً: حزمة عميل HTTP API قابلة لإعادة الاستخدام مع معالجة الأخطاء والمصادقة وتحليل JSON.

حزمة عميل API

// ====== lib/src/api_client.dart ======
import 'dart:convert';
import 'dart:io';

/// تكوين عميل API.
class ApiConfig {
  final String baseUrl;
  final Duration timeout;
  final Map<String, String> defaultHeaders;

  const ApiConfig({
    required this.baseUrl,
    this.timeout = const Duration(seconds: 30),
    this.defaultHeaders = const {},
  });
}

/// استثناء مخصص لأخطاء API.
class ApiException implements Exception {
  final int statusCode;
  final String message;
  final String? body;

  ApiException(this.statusCode, this.message, [this.body]);

  @override
  String toString() => 'ApiException($statusCode): $message';
}

/// غلاف استجابة مع بيانات محددة النوع.
class ApiResponse<T> {
  final int statusCode;
  final T data;
  final Map<String, String> headers;

  ApiResponse({
    required this.statusCode,
    required this.data,
    required this.headers,
  });

  bool get isSuccess => statusCode >= 200 && statusCode < 300;
}

/// عميل API قابل لإعادة الاستخدام مع مصادقة ومعالجة أخطاء.
class ApiClient {
  final ApiConfig config;
  final HttpClient _httpClient;
  String? _authToken;

  ApiClient(this.config) : _httpClient = HttpClient() {
    _httpClient.connectionTimeout = config.timeout;
  }

  /// تعيين رمز مصادقة Bearer.
  void setAuthToken(String token) => _authToken = token;

  /// مسح رمز المصادقة.
  void clearAuth() => _authToken = null;

  /// تنفيذ طلب GET وفك ترميز استجابة JSON.
  Future<ApiResponse<Map<String, dynamic>>> get(String endpoint) async {
    final uri = Uri.parse('${config.baseUrl}$endpoint');
    final request = await _httpClient.getUrl(uri);
    _addHeaders(request);

    final response = await request.close();
    return _processResponse(response);
  }

  /// تنفيذ طلب POST مع جسم JSON.
  Future<ApiResponse<Map<String, dynamic>>> post(
    String endpoint,
    Map<String, dynamic> body,
  ) async {
    final uri = Uri.parse('${config.baseUrl}$endpoint');
    final request = await _httpClient.postUrl(uri);
    _addHeaders(request);
    request.headers.contentType = ContentType.json;
    request.write(jsonEncode(body));

    final response = await request.close();
    return _processResponse(response);
  }

  void _addHeaders(HttpClientRequest request) {
    config.defaultHeaders.forEach((key, value) {
      request.headers.set(key, value);
    });
    if (_authToken != null) {
      request.headers.set('Authorization', 'Bearer $_authToken');
    }
  }

  Future<ApiResponse<Map<String, dynamic>>> _processResponse(
    HttpClientResponse response,
  ) async {
    final body = await response.transform(utf8.decoder).join();
    final data = jsonDecode(body) as Map<String, dynamic>;
    final headers = <String, String>{};
    response.headers.forEach((name, values) {
      headers[name] = values.join(', ');
    });

    if (response.statusCode >= 400) {
      throw ApiException(response.statusCode, 'Request failed', body);
    }

    return ApiResponse(
      statusCode: response.statusCode,
      data: data,
      headers: headers,
    );
  }

  /// إغلاق العميل وتحرير الموارد.
  void close() => _httpClient.close();
}

إصدار الحزم ونشرها

إذا أردت مشاركة حزمتك مع مجتمع Dart، يمكنك نشرها على pub.dev. إليك استراتيجية الإصدار وسير عمل النشر.

قواعد الإصدار الدلالي

# الإصدار الدلالي: MAJOR.MINOR.PATCH
# 1.0.0 -> أول إصدار مستقر

# PATCH (1.0.0 -> 1.0.1): إصلاحات أخطاء، بدون تغييرات API
#   - إصلاح تعطل في parseData()
#   - تصحيح أخطاء مطبعية في التوثيق

# MINOR (1.0.0 -> 1.1.0): ميزات جديدة، متوافقة للخلف
#   - إضافة طريقة StringUtils.reverse()
#   - معامل اختياري جديد في مُنشئ ApiClient

# MAJOR (1.0.0 -> 2.0.0): تغييرات كاسرة
#   - إزالة الطرق المهملة
#   - تغيير نوع إرجاع get() من String إلى ApiResponse
#   - إعادة تسمية ApiConfig إلى ClientConfig

# إصدارات ما قبل الإصدار
# 1.0.0-alpha.1  -> تطوير مبكر
# 1.0.0-beta.1   -> مكتمل الميزات، قد يحتوي أخطاء
# 1.0.0-rc.1     -> مرشح للإصدار

# أوامر النشر
# dart pub publish --dry-run    # التحقق من المشاكل
# dart pub publish              # النشر على pub.dev (لا رجعة فيه!)

# تنسيق CHANGELOG.md:
# ## 1.1.0
# - Added `StringUtils.reverse()` extension method
# - Added `titleCase` getter to StringUtils
# - Fixed issue #42: truncate now handles empty strings
نصيحة: قبل النشر، دائماً شغّل dart pub publish --dry-run للتحقق من المشاكل الشائعة (وصف مفقود، قيود SDK غير صالحة، كود غير منسّق). بمجرد النشر، لا يمكن إلغاء نشر الإصدار — إنه دائم. تأكد أن كودك مختبر وموثّق قبل النشر.

أفضل الممارسات لحزم Dart

اتبع هذه الاتفاقيات لإنشاء حزم احترافية وقابلة للصيانة:

قائمة تحقق أفضل ممارسات الحزم

// 1. التسمية: استخدم أحرف_صغيرة_مع_شرطات_سفلية
//    جيد: my_utils, api_client, date_helper
//    سيئ: myUtils, MyUtils, my-utils

// 2. تنظيم الملفات: ضع الواجهة العامة في lib/ والتنفيذ في lib/src/
//    lib/my_package.dart        # الواجهة العامة (ملف البرميل)
//    lib/src/internal_stuff.dart # التنفيذ الخاص

// 3. التوثيق: استخدم تعليقات التوثيق /// على جميع الواجهات العامة
/// يتحقق من عنوان بريد إلكتروني.
///
/// يعيد `true` إذا تطابق [email] مع تنسيق قياسي.
/// لا يتحقق من أن النطاق موجود فعلاً.
///
/// مثال:
/// ```dart
/// isEmail('user@example.com'); // true
/// isEmail('not-an-email');      // false
/// ```
bool isEmail(String email) { ... }

// 4. التحليل: استخدم خيارات تحليل صارمة
//    analysis_options.yaml:
//    include: package:lints/recommended.yaml

// 5. الاختبار: اكتب اختبارات لجميع الواجهات العامة
//    test/my_package_test.dart

// 6. المثال: قدّم مثالاً قابلاً للتشغيل
//    example/example.dart

// 7. التصديرات: كن صريحاً بشأن واجهتك العامة
//    افعل:   export 'src/models.dart' show User, Product;
//    تجنب: export 'src/models.dart'; (يصدّر كل شيء)

الملخص

في هذا الدرس، تعلمت نظام حزم Dart الكامل:

  • pubspec.yaml — ملف التكوين الذي يحدد هوية حزمتك وقيود SDK والتبعيات
  • pub.dev — البحث عن الحزم وتقييمها وإضافتها مع dart pub add
  • قيود الإصدار — صيغة القيراط والنطاقات وتبعيات git وتبعيات المسار
  • تنظيم المكتبة — ملفات البرميل و export و show/hide والاستيرادات بالبادئات
  • part/part of — تقسيم المكتبات عبر الملفات (تُستخدم أساساً مع توليد الكود)
  • الاستيرادات المؤجلة — التحميل الكسول للمكتبات لتقسيم كود الويب
  • إنشاء الحزم — هيكل المجلدات واتفاقيات التسمية والتوثيق
  • النشر — الإصدار الدلالي وفحوصات التشغيل التجريبي وسير عمل النشر
  • أفضل الممارسات — التسمية وتنظيم الملفات والتوثيق والاختبار والتحليل