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

مقدمة إلى Dio: الإعداد والطلبات الأساسية

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

مقدمة إلى Dio: الإعداد والطلبات الأساسية

حزمة http بسيطة وفعّالة للشبكات الأساسية، لكن تطبيقات Flutter في بيئات الإنتاج تحتاج بسرعة إلى المزيد: تهيئة عنوان URL أساسي تلقائي، رؤوس افتراضية مركزية، معترضات (interceptors) للطلب والاستجابة، نماذج أخطاء غنية، ودعم مدمج لبيانات النموذج ورفع الملفات. Dio هو عميل HTTP قوي لـ Dart يغطي جميع هذه الاحتياجات مباشرة من الصندوق. في هذا الدرس ستتعلم كيفية تثبيت Dio وتهيئته، وإرفاق عنوان URL أساسي ورؤوس افتراضية بنموذج Dio واحد، وتنفيذ نفس طلبات GET وPOST وPUT وDELETE التي تعرفها بالفعل — باستخدام واجهة برمجة Dio النظيفة والمتسقة.

لماذا تختار Dio على حزمة http؟

كلتا الحزمتين تستطيعان إرسال طلبات HTTP، لكنهما تختلفان بشكل كبير في النطاق:

  • عنوان URL الأساسي والخيارات: يقبل Dio كائن BaseOptions عند الإنشاء، بحيث لا تحتاج أبداً إلى ربط المضيف في كل رابط طلب.
  • المعترضات (Interceptors): يمتلك Dio خط أنابيب معترضات متكامل للتسجيل وحقن الرمز المميز والتحديث التلقائي للرمز — دون الحاجة إلى أغلفة.
  • معالجة الأخطاء: أخطاء الشبكة واستجابات 4xx/5xx وانتهاء المهلة تُرمي جميعها DioException مكتوباً النوع مع تعداد DioExceptionType، مما يجعل فروع الأخطاء واضحة.
  • FormData ورفع الملفات: يوفر Dio FormData وMultipartFile دون حزم إضافية.
  • الإلغاء: يمكن إلغاء الطلبات عبر CancelToken.
ملاحظة: لا يحل Dio محل حزمة http في جميع المشاريع. بالنسبة للتطبيقات الصغيرة أو الحزم ذات التبعيات الضئيلة، تعد حزمة http الأخف وزناً خياراً جيداً. يتميز Dio عندما تحتاج إلى عميل HTTP جاهز للإنتاج بميزات متقدمة.

الخطوة الأولى — إضافة Dio إلى مشروعك

افتح pubspec.yaml وأضف التبعية، ثم شغّل flutter pub get:

# pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  dio: ^5.4.0        # استخدم أحدث إصدار مستقر

بعد جلب الحزم، استورد Dio أينما تحتاجه:

import 'package:dio/dio.dart';

الخطوة الثانية — إنشاء وتهيئة نموذج Dio

النمط الموصى به هو إنشاء نموذج Dio واحد مشترك (singleton أو فئة خدمة) وتهيئته مرة واحدة باستخدام BaseOptions. كل طلب يتم عبر ذلك النموذج يرث تلقائياً عنوان URL الأساسي والرؤوس والمهل الزمنية.

import 'package:dio/dio.dart';

class ApiClient {
  static final Dio _dio = Dio(
    BaseOptions(
      baseUrl: 'https://api.example.com/v1/',
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 15),
      headers: {
        'Accept': 'application/json',
        'Content-Type': 'application/json',
      },
    ),
  );

  // كشف النموذج المُهيَّأ
  static Dio get instance => _dio;
}
نصيحة: احرص دائماً على ضبط كل من connectTimeout وreceiveTimeout. بدونهما، يمكن لخادم بطيء أن يجمّد تطبيقك إلى أجل غير مسمى. نقطة بداية جيدة هي 10 ثوانٍ للاتصال و15 ثانية للاستقبال.

الخطوة الثالثة — GET وPOST وPUT وDELETE مع Dio

تعكس أساليب Dio أفعال HTTP وتُرجع Future<Response>. يحمل كائن Response الـ data المُفككة وstatusCode وheaders والمزيد.

import 'package:dio/dio.dart';

class PostService {
  final Dio _dio;

  PostService(this._dio);

  // GET — جلب قائمة من المنشورات
  Future<List<dynamic>> fetchPosts() async {
    final Response response = await _dio.get('posts');
    // response.data هو JSON مُفكَّك بالفعل (List أو Map)
    return response.data as List<dynamic>;
  }

  // GET مع معاملات الاستعلام
  Future<Map<String, dynamic>> fetchPost(int id) async {
    final Response response = await _dio.get(
      'posts/$id',
      queryParameters: {'include': 'comments'},
    );
    return response.data as Map<String, dynamic>;
  }

  // POST — إنشاء منشور جديد
  Future<Map<String, dynamic>> createPost(String title, String body) async {
    final Response response = await _dio.post(
      'posts',
      data: {'title': title, 'body': body, 'userId': 1},
    );
    return response.data as Map<String, dynamic>;
  }

  // PUT — استبدال منشور موجود
  Future<Map<String, dynamic>> updatePost(int id, String title) async {
    final Response response = await _dio.put(
      'posts/$id',
      data: {'title': title, 'body': 'Updated body', 'userId': 1},
    );
    return response.data as Map<String, dynamic>;
  }

  // DELETE — حذف منشور
  Future<void> deletePost(int id) async {
    await _dio.delete('posts/$id');
  }
}
ملاحظة: على خلاف حزمة http، يقوم Dio تلقائياً بفك تشفير جسم استجابة JSON إلى Map أو List في Dart. لا تحتاج إلى استدعاء jsonDecode() يدوياً — النتيجة المُفككة متاحة مباشرة على response.data.

الخطوة الرابعة — معالجة الأخطاء مع DioException

يُرمي Dio استثناء DioException (المعروف سابقاً بـ DioError في الإصدار 4) عند حدوث فشل في الشبكة أو انتهاء المهلة أو استجابات غير 2xx. خاصية type الخاصة به هي تعداد DioExceptionType يتيح لك التفريع بشكل نظيف:

Future<void> safeFetch() async {
  try {
    final response = await ApiClient.instance.get('posts/1');
    print('Status: ${response.statusCode}');
    print('Data: ${response.data}');
  } on DioException catch (e) {
    switch (e.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.receiveTimeout:
        print('انتهت مهلة الطلب.');
        break;
      case DioExceptionType.badResponse:
        // الخادم رد بحالة غير 2xx
        final status = e.response?.statusCode;
        print('خطأ في الخادم: $status — ${e.response?.data}');
        break;
      case DioExceptionType.connectionError:
        print('لا يوجد اتصال بالإنترنت.');
        break;
      default:
        print('خطأ غير متوقع: ${e.message}');
    }
  }
}
تحذير: في Dio الإصدار 5 وما بعده، تم إعادة تسمية DioError إلى DioException. إذا كنت تتبع دروساً قديمة تلتقط DioError، حدّث جمل catch إلى DioException لتجنب التفويتات الصامتة في وقت التشغيل.

الخلاصة

في هذا الدرس تعلمت كيفية إضافة Dio إلى مشروع Flutter وتهيئة نموذج Dio مشترك بعنوان URL أساسي ورؤوس افتراضية ومهل زمنية عبر BaseOptions. ثم أعدت إنتاج مجموعة كاملة من طلبات CRUD — GET وPOST وPUT وDELETE — باستخدام أساليب Dio الموجزة للأفعال، وتعاملت مع الأخطاء من خلال DioException المكتوب النوع. مقارنةً بحزمة http المجردة، يزيل Dio الكثير من الكود المتكرر (لا حاجة لـ jsonDecode يدوياً، لا ربط لعناوين URL، لا تكرار للرؤوس) ويضع الأساس للمعترضات والميزات المتقدمة التي تُغطى في الدروس القادمة.