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

إرسال طلبات GET وتحليل استجابات JSON

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

إرسال طلبات GET وتحليل استجابات JSON

تحتاج كل تطبيقات Flutter العملية تقريباً إلى التواصل مع خادم بعيد. العملية الأكثر شيوعاً هي طلب GET — طلب البيانات من الخادم — تليها عملية التحليل لتحويل تلك البيانات من JSON إلى كائنات Dart. في هذا الدرس ستتعلم سير العمل الكامل: إضافة حزمة http، إرسال طلب GET، فحص الاستجابة، وتحويل سلسلة JSON الخام إلى خرائط وقوائم Dart قابلة للاستخدام باستخدام dart:convert.

حزمة http

لا يأتي Flutter مع عميل HTTP مدمج (يوجد HttpClient في dart:io لكنه مطوّل). الحل المعياري في المجتمع هو حزمة http المنشورة من قِبل فريق Dart. أضفها إلى pubspec.yaml:

إضافة التبعية في pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  http: ^1.2.1          # تحقق دائماً من أحدث إصدار على pub.dev

نفّذ flutter pub get لتنزيلها. ثم استورد كلتا المكتبتين في أعلى ملف Dart الذي سينفذ الطلب:

الاستيرادات المطلوبة

import 'package:http/http.dart' as http;
import 'dart:convert';
ملاحظة: استيراد http باستخدام اللقب as http هو الأسلوب المعتاد. يمنع تعارض الأسماء ويجعل من الواضح فوراً أن http.get() تأتي من الحزمة وليس من كودك.

تنفيذ طلب GET

http.get() دالة غير متزامنة (async) تُرجع Future<http.Response>. يجب أن تنتظرها بـ await داخل دالة async. المصنع Uri.parse() يحوّل سلسلة URL العادية إلى نوع Uri الذي تتوقعه الحزمة.

طلب GET أساسي

import 'package:http/http.dart' as http;
import 'dart:convert';

Future<void> fetchUsers() async {
  final uri = Uri.parse('https://jsonplaceholder.typicode.com/users');

  final response = await http.get(uri);

  if (response.statusCode == 200) {
    // نجاح — الجسم سلسلة JSON
    print('نص الاستجابة: ${response.body}');
  } else {
    // غير 2xx يعني حدث خطأ ما
    throw Exception('فشل تحميل المستخدمين: ${response.statusCode}');
  }
}

كائن http.Response يكشف عدة أعضاء مفيدة:

  • response.statusCode — رمز حالة HTTP (200، 404، 500، إلخ)
  • response.body — جسم الاستجابة كـ String
  • response.headers — خريطة Map<String, String> لترويسات الاستجابة
  • response.bodyBytes — الجسم الخام كـ Uint8List (مفيد للبيانات الثنائية)

تحليل JSON باستخدام dart:convert

مكتبة dart:convert توفر الدالة العلوية jsonDecode(). تقبل سلسلة JSON وتُرجع قيمة dynamic في Dart — إما Map<String, dynamic> (لكائن JSON) أو List<dynamic> (لمصفوفة JSON). ثم تُحوّل وتصل إلى الحقول بالاسم.

تحليل مصفوفة JSON من الكائنات

Future<List<Map<String, dynamic>>> fetchPosts() async {
  final uri = Uri.parse('https://jsonplaceholder.typicode.com/posts');
  final response = await http.get(uri);

  if (response.statusCode != 200) {
    throw Exception('HTTP ${response.statusCode}');
  }

  // jsonDecode يُرجع dynamic — حوّله إلى الشكل المتوقع
  final List<dynamic> jsonList = jsonDecode(response.body) as List<dynamic>;

  // تحويل كل عنصر إلى Map مُحدد النوع
  final posts = jsonList
      .map((item) => item as Map<String, dynamic>)
      .toList();

  for (final post in posts) {
    print('العنوان: ${post['title']}');
    print('الجسم: ${post['body']}');
    print('معرف المستخدم: ${post['userId']}');
    print('---');
  }

  return posts;
}
نصيحة: في كود الإنتاج تقوم دائماً تقريباً بتعيين هذه الخرائط الخام إلى فئات نموذج Dart مناسبة مع منشئ مصنع fromJson. هذه الخطوة مغطاة في الدرس التالي. في الوقت الحالي، العمل مع الخرائط الخام يتيح لك فهم ما تُرجعه jsonDecode فعلياً قبل إضافة طبقة تجريد إضافية.

تحليل كائن JSON واحد

عندما يُرجع الخادم كائن JSON واحداً (وليس مصفوفة)، تُرجع jsonDecode Map<String, dynamic>. استخدم بناء جملة القوسين المألوف للوصول إلى الحقول:

تحليل كائن JSON

Future<Map<String, dynamic>> fetchUser(int id) async {
  final uri = Uri.parse('https://jsonplaceholder.typicode.com/users/$id');
  final response = await http.get(uri);

  if (response.statusCode != 200) {
    throw Exception('المستخدم غير موجود (${response.statusCode})');
  }

  final Map<String, dynamic> user =
      jsonDecode(response.body) as Map<String, dynamic>;

  // الوصول إلى الحقول الفردية
  final String name    = user['name']  as String;
  final String email   = user['email'] as String;
  final Map<String, dynamic> address =
      user['address'] as Map<String, dynamic>;
  final String city    = address['city'] as String;

  print('الاسم: $name، البريد: $email، المدينة: $city');
  return user;
}
تحذير: تُرجع jsonDecode() قيمة dynamic. إذا وصلت إلى حقل غير موجود في JSON، يُرجع Dart القيمة null وقت التشغيل — لا يُطلق خطأ وقت الترجمة. تحقق دائماً من null أو استخدم ?.cast<>() عندما قد يكون الحقل غائباً، خاصة مع حقول API الاختيارية أو القابلة للـ null.

معالجة الأخطاء والاستثناءات الشبكية

قد تفشل مكالمات الشبكة لأسباب تتجاوز رمز الحالة السيئ: لا إنترنت، فشل DNS، انتهاء مهلة الخادم، أو سلسلة JSON مشوهة. لف طلبك في try-catch للتعامل بأناقة مع أخطاء HTTP واستثناءات المقبس:

جلب قوي مع معالجة الأخطاء

import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:io';          // لـ SocketException

Future<List<dynamic>> safeFetchPosts() async {
  try {
    final response = await http
        .get(Uri.parse('https://jsonplaceholder.typicode.com/posts'))
        .timeout(const Duration(seconds: 10));

    if (response.statusCode == 200) {
      return jsonDecode(response.body) as List<dynamic>;
    } else {
      throw Exception('خطأ الخادم: ${response.statusCode}');
    }
  } on SocketException {
    throw Exception('لا يوجد اتصال بالإنترنت');
  } on FormatException {
    throw Exception('تنسيق استجابة خاطئ — تعذر تحليل JSON');
  }
}

ملخص

لإرسال طلب GET وتحليل JSON في Flutter: أضف http إلى التبعيات، استورد http وdart:convert، استدعِ await http.get(Uri.parse(url))، تحقق من response.statusCode == 200، ومرر response.body إلى jsonDecode(). النتيجة إما Map<String, dynamic> أو List<dynamic> يمكنك التكرار عليها أو تحويلها. لف الاستدعاء في try-catch للتعامل مع مشاكل الاتصال والاستجابات المشوهة.

النقطة الرئيسية: http.get() مع jsonDecode() يشكلان أساس كل عمليات الشبكة في Flutter. كل نمط ذي مستوى أعلى — فئات النماذج، المستودعات، تكامل FutureBuilder — يُبنى فوق هذين الأساسيين.