الشبكات وتكامل REST API

تسلسل JSON اليدوي مع فئات النماذج

16 دقيقة الدرس 4 من 13

تسلسل JSON اليدوي مع فئات النماذج

عندما يجلب تطبيق Flutter بيانات من واجهة برمجة تطبيقات REST، تصل الاستجابة بصيغة JSON خام — وهي في جوهرها Map<String, dynamic> في Dart. الوصول إلى القيم بنمط data['user']['email'] في كل أنحاء الكود هش وصعب إعادة الهيكلة ولا يوفر أي أمان في وقت الترجمة. فئات النماذج مع دوال fromJson وtoJson تحل هذه المشكلة بمنحك كائنات مكتوبة ومسماة تمثل موارد API.

لماذا تهم فئات النماذج المكتوبة

الوصول إلى الخرائط الخام يعاني من عدة عيوب خطيرة في كود الإنتاج:

  • لا أمان في الأنواع: data['age'] قد يُرجع int أو String أو null — ولا يستطيع المترجم تحذيرك.
  • أخطاء إملائية في المفاتيح: مفتاح مهجوء خطأً مثل data['emial'] يُرجع null بصمت عوضاً عن إطلاق خطأ وقت الترجمة.
  • لا إكمال تلقائي في بيئة التطوير: لا تستطيع بيئة التطوير اقتراح أسماء الحقول على Map عادية.
  • منطق تحليل منتشر: يتكرر تحليل JSON عبر الودجات والمستودعات والخدمات.

تقوم فئة النموذج بمركزة كل منطق تحليل JSON في مكان واحد، وتسمح لنظام الأنواع بالتحقق من كودك، وتجعل إعادة الهيكلة أمراً بسيطاً.

تشريح فئة نموذج Dart

تحتوي فئة النموذج المنظمة جيداً على ثلاثة أشياء: حقول نهائية، ومُنشئ factory مسمى fromJson يحلل الخريطة، ودالة toJson تُسلسل الكائن مرة أخرى إلى خريطة.

نموذج أساسي: User

class User {
  final int id;
  final String name;
  final String email;
  final String? avatarUrl; // قابل للقيمة الفارغة — قد يغيب من JSON

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

  /// يحلل [User] من خريطة JSON مُرجعة من API.
  factory User.fromJson(Map<String, dynamic> json) {
    return User(
      id: json['id'] as int,
      name: json['name'] as String,
      email: json['email'] as String,
      avatarUrl: json['avatar_url'] as String?,
    );
  }

  /// يحول هذا [User] إلى خريطة JSON (مفيد لطلبات POST/PUT).
  Map<String, dynamic> toJson() {
    return {
      'id': id,
      'name': name,
      'email': email,
      if (avatarUrl != null) 'avatar_url': avatarUrl,
    };
  }
}
ملاحظة: كلمة factory تشير إلى أن المُنشئ قد يُرجع نسخة مخزنة مؤقتاً أو يفوّض لفئة فرعية — لكن في تحليل النماذج هو ببساطة الاسم التقليدي لمُنشئ مسمى يبني الكائن من بيانات خارجية. يمكنك أيضاً استخدام مُنشئ عادي مسمى؛ factory هو النمط الاصطلاحي لتحليل JSON.

معالجة الكائنات المتداخلة والقوائم

نادراً ما تُرجع APIs الحقيقية JSON مسطحاً. ستواجه في كثير من الأحيان كائنات متداخلة ومصفوفات. كل نوع متداخل يحصل على فئة نموذج خاصة به، وfromJson يفوّض إلى تلك الفئات.

نموذج متداخل: Post مع Author وتعليقات

class Author {
  final int id;
  final String username;

  const Author({required this.id, required this.username});

  factory Author.fromJson(Map<String, dynamic> json) => Author(
        id: json['id'] as int,
        username: json['username'] as String,
      );

  Map<String, dynamic> toJson() => {'id': id, 'username': username};
}

class Post {
  final int id;
  final String title;
  final String body;
  final Author author;
  final List<String> tags;
  final DateTime publishedAt;

  const Post({
    required this.id,
    required this.title,
    required this.body,
    required this.author,
    required this.tags,
    required this.publishedAt,
  });

  factory Post.fromJson(Map<String, dynamic> json) {
    return Post(
      id: json['id'] as int,
      title: json['title'] as String,
      body: json['body'] as String,
      // تفويض تحليل الكائن المتداخل إلى Author.fromJson
      author: Author.fromJson(json['author'] as Map<String, dynamic>),
      // تحليل مصفوفة JSON من سلاسل نصية إلى List<String>
      tags: List<String>.from(json['tags'] as List),
      // تحليل سلسلة تاريخ ISO 8601 إلى DateTime
      publishedAt: DateTime.parse(json['published_at'] as String),
    );
  }

  Map<String, dynamic> toJson() => {
        'id': id,
        'title': title,
        'body': body,
        'author': author.toJson(),
        'tags': tags,
        'published_at': publishedAt.toIso8601String(),
      };
}

تحليل استجابات API عملياً

بعد جلب JSON باستخدام http أو dio، تفكّك جسم الاستجابة بـjsonDecode، ثم تمرر الخريطة الناتجة إلى مصنع fromJson في نموذجك.

نصيحة: احرص دائماً على تحويل نتيجة jsonDecode إلى النوع الدقيق الذي تتوقعه (Map<String, dynamic> للكائنات، List<dynamic> للمصفوفات). التحويل المفقود يسبب TypeError في وقت التشغيل يصعب تشخيصه مقارنةً بخطأ واضح من نموذجك.

التحليل الدفاعي: معالجة الحقول المفقودة والفارغة

لا تكون APIs دائماً منضبطة. قد تكون الحقول غائبة أو null بشكل غير متوقع أو تصل بنوع خاطئ. استخدم هذه الاستراتيجيات لكتابة مصانع fromJson قوية:

  • الحقول القابلة للقيمة الفارغة: أعلن الحقل كـString? وحوّله بـjson['key'] as String?.
  • القيم الافتراضية: استخدم عامل دمج الفارغ: (json['score'] as int?) ?? 0.
  • حقول القوائم المفقودة: (json['tags'] as List?)?.map((e) => e as String).toList() ?? [].
  • تحويل الأنواع: إذا كانت API تُرجع أحياناً int وأحياناً double لحقل رقمي، استخدم (json['price'] as num).toDouble().

نمط copyWith

فئات النماذج في Flutter عادةً غير قابلة للتغيير. لإنشاء نسخة معدّلة من كائن (شائع في إدارة الحالة)، أضف دالة copyWith تُرجع نسخة جديدة مع تجاوز حقول مختارة.

تحذير: لا تضف أبداً ضابطات قابلة للتغيير إلى فئات النماذج. النماذج القابلة للتغيير تؤدي إلى أخطاء الحالة المشتركة التي يصعب للغاية تتبعها — خاصةً في الكود غير المتزامن. احتفظ بكل حقل كـfinal واستخدم copyWith لإنتاج نسخ محدّثة.

ملخص

يمنح تسلسل JSON اليدوي مع fromJson وtoJson تطبيق Flutter طبقة بيانات آمنة الأنواع دون أي توليد للكود. تمركز كل منطق التحليل في فئات النماذج، وتكسب الإكمال التلقائي في بيئة التطوير والفحص بالمترجم، وتجعل كود المستودعات وواجهة المستخدم أكثر نظافة وصيانة. مع نمو مشروعك، يمكنك الانتقال إلى أدوات توليد الكود كـjson_serializable — لكن النمط اليدوي يبقى أساسياً لفهم ما تنتجه تلك الأدوات.