ديو (Dio): حزمة الشبكات الأسطورية لـ Dart/Flutter
مقدمة: سيمفونية الاتصال بالشبكة مع Dio
في عالم تطوير تطبيقات Dart و Flutter، حيث تتشابك الخيوط الرقمية لتنسج تجارب المستخدمين، تبرز حزمة Dio كقوة لا يستهان بها في مجال الشبكات. إنها ليست مجرد مكتبة HTTP، بل هي أوركسترا متكاملة تُعزف فيها سيمفونية الاتصال بالشبكة بسلاسة واحترافية. صُممت Dio لتكون رفيقك الأمين في رحلة بناء تطبيقات تتطلب تفاعلاً قوياً وموثوقاً مع الخوادم، مقدمةً مجموعة واسعة من الميزات التي تجعل التعامل مع طلبات HTTP متعة حقيقية.
تُقدم Dio دعماً لا مثيل له لمجموعة من الميزات الأساسية والمتقدمة، والتي تشمل على سبيل المثال لا الحصر:
- التكوين العالمي (Global Configuration): القدرة على ضبط إعدادات عامة لجميع طلبات HTTP، مثل الروابط الأساسية (Base URLs)، والمهل الزمنية (Timeouts)، والعناوين (Headers)، مما يوفر اتساقاً ويسهل إدارة الإعدادات.
- المراقبون (Interceptors): آلية قوية لاعتراض وتعديل الطلبات والاستجابات والأخطاء قبل معالجتها. تخيل وجود حراس بوابة يمكنهم فحص، تعديل، أو حتى منع مرور البيانات بناءً على قواعد محددة.
- بيانات النماذج (FormData): دعم سلس لإرسال البيانات المعقدة، بما في ذلك الملفات، عبر طلبات
multipart/form-data، وهو أمر حيوي لعمليات رفع الملفات والصور. - إلغاء الطلبات (Request Cancellation): القدرة على إلغاء الطلبات الجارية، مما يمنع استهلاك الموارد غير الضروري ويحسن من استجابة التطبيق، خاصة في سيناريوهات البحث أو التحديث المتكرر.
- رفع وتنزيل الملفات (File Uploading/Downloading): وظائف مدمجة وفعالة للتعامل مع عمليات نقل الملفات الكبيرة والصغيرة، مع إمكانية تتبع التقدم.
- المهل الزمنية (Timeout): التحكم الدقيق في المدة التي يجب أن ينتظرها الطلب للاستجابة، مما يمنع تعليق التطبيق بسبب خوادم بطيئة أو غير مستجيبة.
- المحولات المخصصة (Custom Adapters): المرونة في استخدام محولات HTTP مخصصة، مما يتيح التكامل مع مكتبات HTTP أخرى أو التعامل مع بيئات شبكة فريدة.
- المحولون (Transformers): أدوات لتحويل بيانات الطلب والاستجابة، مثل تحويل JSON إلى كائنات Dart والعكس، مما يبسط عملية التعامل مع البيانات.
نداء إلى الأبطال! لا تنسَ إضافة الوسم
#dioإلى حزمك المنشورة المتعلقة بـ Dio على pub.dev! دع العالم يعرف إبداعاتك التي تستخدم هذه الحزمة الأسطورية. لمزيد من التفاصيل حول المواضيع فيpubspec.yaml، راجع وثائق Dart الرسمية.
جدول المحتويات: خريطتك إلى عالم Dio
* [ديو (Dio): حزمة الشبكات الأسطورية لـ Dart/Flutter](#ديو-dio-حزمة-الشبكات-الأسطورية-لـ-dartflutter) * [مقدمة: سيمفونية الاتصال بالشبكة مع Dio](#مقدمة-سيمفونية-الاتصال-بالشبكة-مع-dio) * [البداية: خطوتك الأولى نحو العظمة](#البداية-خطوتك-الأولى-نحو-العظمة) * [التثبيت: إطلاق العنان لقوة Dio](#التثبيت-إطلاق-العنان-لقوة-dio) * [سهولة فائقة في الاستخدام: سحر Dio في بضعة أسطر](#سهولة-فائقة-في-الاستخدام-سحر-dio-في-بضعة-أسطر) * [عالم Dio المذهل: كنوز تنتظرك](#عالم-dio-المذهل-كنوز-تنتظرك) * [الإضافات (Plugins): توسيع آفاق Dio](#الإضافات-plugins-توسيع-آفاق-dio) * [أمثلة: رحلة عبر قدرات Dio](#أمثلة-رحلة-عبر-قدرات-dio) * [إجراء طلب `GET`: استكشاف البيانات](#إجراء-طلب-get-استكشاف-البيانات) * [إجراء طلب `POST`: إرسال البيانات إلى المجهول](#إجراء-طلب-post-إرسال-البيانات-إلى-المجهول) * [إجراء طلبات متعددة متزامنة: قوة التوازي](#إجراء-طلبات-متعددة-متزامنة-قوة-التوازي) * [تنزيل ملف: جلب الكنوز الرقمية](#تنزيل-ملف-جلب-الكنوز-الرقمية) * [الحصول على تدفق الاستجابة (Response Stream): تدفق البيانات بلا حدود](#الحصول-على-تدفق-الاستجابة-response-stream-تدفق-البيانات-بلا-حدود) * [الحصول على الاستجابة بالبايتات (Bytes): الغوص في أعماق البيانات الخام](#الحصول-على-الاستجابة-بالبايتات-bytes-الغوص-في-أعماق-البيانات-الخام) * [إرسال `FormData`: بناء الجسور للبيانات المعقدة](#إرسال-formdata-بناء-الجسور-للبيانات-المعقدة) * [رفع ملفات متعددة عبر FormData: إطلاق العنان لفيض البيانات](#رفع-ملفات-متعددة-عبر-formdata-إطلاق-العنان-لفيض-البيانات) * [الاستماع لتقدم الرفع: مراقبة رحلة البيانات](#الاستماع-لتقدم-الرفع-مراقبة-رحلة-البيانات) * [إرسال بيانات ثنائية عبر Stream: تدفق البايتات ببراعة](#إرسال-بيانات-ثنائية-عبر-stream-تدفق-البايتات-ببراعة) * [واجهات برمجة تطبيقات Dio (Dio APIs): قلب Dio النابض](#واجهات-برمجة-تطبيقات-dio-dio-apis-قلب-dio-النابض) * [إنشاء مثيل وضبط التكوينات الافتراضية: صياغة أداتك](#إنشاء-مثيل-وضبط-التكوينات-الافتراضية-صياغة-أداتك) * [خيارات الطلب (Request Options): توجيه سفينتك](#خيارات-الطلب-request-options-توجيه-سفينتك) * [الاستجابة (Response): حصاد رحلتك](#الاستجابة-response-حصاد-رحلتك) * [المراقبون (Interceptors): حراس البوابة الأوفياء](#المراقبون-interceptors-حراس-البوابة-الأوفياء) * [حل ورفض الطلب (Resolve and reject the request): التحكم المطلق](#حل-ورفض-الطلب-resolve-and-reject-the-request-التحكم-المطلق) * [المراقب المجدول (QueuedInterceptor): التنفيذ المتسلسل بذكاء](#المراقب-المجدول-queuedinterceptor-التنفيذ-المتسلسل-بذكاء) * [مثال: حماية بياناتك بـ CSRF Token](#مثال-حماية-بياناتك-بـ-csrf-token) * [مراقب السجلات (LogInterceptor): عيون Dio الساهرة](#مراقب-السجلات-loginterceptor-عيون-dio-الساهرة) * [Dart: تسجيل السجلات في بيئة Dart](#dart-تسجيل-السجلات-في-بيئة-dart) * [Flutter: تسجيل السجلات في بيئة Flutter](#flutter-تسجيل-السجلات-في-بيئة-flutter) * [مراقب مخصص (Custom Interceptor): إبداعك بلا حدود](#مراقب-مخصص-custom-interceptor-إبداعك-بلا-حدود) * [التعامل مع الأخطاء (Handling Errors): فن إدارة التحديات](#التعامل-مع-الأخطاء-handling-errors-فن-إدارة-التحديات) * [DioException: استثناءات Dio المخصصة](#dioexception-استثناءات-dio-المخصصة) * [DioExceptionType: أنواع الأخطاء، مفتاح الفهم](#dioexceptiontype-أنواع-الأخطاء-مفتاح-الفهم) * [استخدام تنسيق application/x-www-form-urlencoded: البيانات التقليدية بأسلوب حديث](#استخدام-تنسيق-applicationx-www-form-urlencoded-البيانات-التقليدية-بأسلوب-حديث) * [إرسال FormData: قوة النماذج المتعددة الأجزاء](#إرسال-formdata-قوة-النماذج-المتعددة-الأجزاء) * [رفع ملفات متعددة (Multiple files upload): إطلاق العنان لفيض البيانات](#رفع-ملفات-متعددة-multiple-files-upload-إطلاق-العنان-لفيض-البيانات) * [إعادة استخدام `FormData` و `MultipartFile`: حكمة الاستخدام الأمثل](#إعادة-استخدام-formdata-و-multipartfile-حكمة-الاستخدام-الأمثل) * [المحول (Transformer): سحرة البيانات](#المحول-transformer-سحرة-البيانات) * [مثال على المحول (Transformer example): تخصيص التحويل](#مثال-على-المحول-transformer-example-تخصيص-التحويل) * [محول عميل HTTP (HttpClientAdapter): الجسر إلى عالم HTTP](#محول-عميل-http-httpclientadapter-الجسر-إلى-عالم-http) * [استخدام البروكسي (Proxy): نافذتك إلى الشبكة](#استخدام-البروكسي-proxy-نافذتك-إلى-الشبكة) * [التحقق من شهادة HTTPS (HTTPS certificate verification): درع الأمان](#التحقق-من-شهادة-https-https-certificate-verification-درع-الأمان) * [دعم HTTP/2: سرعة وكفاءة المستقبل](#دعم-http2-سرعة-وكفاءة-المستقبل) * [الإلغاء (Cancellation): فن التراجع بذكاء](#الإلغاء-cancellation-فن-التراجع-بذكاء) * [توسيع فئة Dio (Extends Dio class): بناء إمبراطوريتك الخاصة](#توسيع-فئة-dio-extends-dio-class-بناء-إمبراطوريتك-الخاصة) * [مشاركة الموارد عبر الأصول على الويب (CORS): عبور الحدود الرقمية](#مشاركة-الموارد-عبر-الأصول-على-الويب-cors-عبور-الحدود-الرقمية)البداية: خطوتك الأولى نحو العظمة
لكل رحلة عظيمة بداية، وبدايتك مع Dio تبدأ بخطوات بسيطة لكنها حاسمة. لننطلق معاً في هذه الرحلة المذهلة.
التثبيت: إطلاق العنان لقوة Dio
لإطلاق العنان لقوة Dio في مشروعك، ما عليك سوى إضافة الحزمة إلى ملف pubspec.yaml الخاص بك. هذا الملف هو بمثابة دستور مشروعك، حيث تُعلن فيه عن جميع التبعيات التي يحتاجها تطبيقك.
افتح ملف pubspec.yaml وأضف dio ضمن قسم dependencies:
dependencies:
flutter:
sdk: flutter
dio: ^5.4.0 # تأكد من استخدام أحدث إصدار متاح
بعد إضافة التبعية، قم بتشغيل الأمر التالي في طرفية مشروعك لجلب الحزمة:
flutter pub get
# أو
dart pub get
بهذه الخطوة البسيطة، تكون قد جهزت مشروعك لاستقبال قوة Dio.
تنبيه قبل الترقية: قد تحدث تغييرات جذرية (Breaking changes) في الإصدارات الرئيسية والفرعية للحزم.
تذكر دائمًا أن عالم البرمجيات يتطور باستمرار. قد تُقدم الإصدارات الجديدة من الحزم تغييرات جذرية (Breaking Changes) تتطلب تعديلات في الكود الخاص بك. لتجنب المفاجآت غير السارة، يُنصح بشدة بمراجعة دليل الهجرة (Migration Guide) الخاص بـ Dio قبل ترقية الإصدار. هذا الدليل هو بوصلتك لتجاوز أي تحديات قد تواجهها أثناء الترقية، ويحتوي على القائمة الكاملة للتغييرات التي قد تؤثر على مشروعك.
سهولة فائقة في الاستخدام: سحر Dio في بضعة أسطر
ما يميز Dio حقًا هو بساطتها وقوتها في آن واحد. يمكنك البدء في إجراء طلبات HTTP ببضعة أسطر من الكود، وكأنك تُلقي تعويذة سحرية لجلب البيانات من عوالم الشبكة.
لنلقِ نظرة على مثال يوضح مدى سهولة استخدام Dio لإجراء طلب GET بسيط:
import 'package:dio/dio.dart'; // استيراد حزمة Dio
// إنشاء مثيل واحد (Singleton) من Dio. يُنصح بذلك لإدارة التكوينات بشكل متسق.
final dio = Dio();
// دالة غير متزامنة (async) لإجراء طلب HTTP
void getHttp() async {
try {
// إجراء طلب GET إلى الرابط المحدد وانتظار الاستجابة
final response = await dio.get('https://dart.dev');
// طباعة الاستجابة الكاملة إلى وحدة التحكم (console)
print(response);
} on DioException catch (e) {
// التعامل مع أي أخطاء قد تحدث أثناء الطلب
print('حدث خطأ: $e');
}
}
// يمكنك استدعاء الدالة في نقطة دخول تطبيقك، مثلاً في دالة main أو عند حدث معين
void main() {
getHttp();
}
في هذا المثال:
- نقوم باستيراد حزمة
dio. - نُنشئ مثيلاً من
Dio، والذي سيكون نقطة الدخول لجميع طلبات الشبكة. - نُعرف دالة
getHttpغير المتزامنة (async) لإجراء الطلب. - باستخدام
await dio.get('https://dart.dev')، نُرسل طلبGETإلى الموقع المحدد وننتظر حتى تصل الاستجابة. - نطبع الاستجابة التي تحتوي على البيانات، العناوين، وحالة الطلب.
- نضيف كتلة
try-catchللتعامل مع أيDioExceptionقد تحدث، مما يجعل تطبيقك أكثر قوة.
بهذه البساطة، تكون قد أجريت أول طلب HTTP لك باستخدام Dio! هذا هو جوهر القوة والسهولة التي تقدمها هذه الحزمة.
عالم Dio المذهل: كنوز تنتظرك
Dio ليست مجرد أداة لإجراء الطلبات الأساسية، بل هي عالم واسع من الإمكانيات التي تنتظر من يكتشفها. إنها تقدم مجموعة من الميزات التي تجعلها الخيار الأمثل للمطورين الذين يبحثون عن حلول شبكات قوية ومرنة.
🎉 قائمة منسقة من الأشياء الرائعة المتعلقة بـ Dio.
هذا القسم هو بمثابة دليل لأفضل الممارسات والموارد المتعلقة بـ Dio، حيث ستجد كنوزًا من الإضافات والأدوات التي تُثري تجربتك مع هذه الحزمة.
الإضافات (Plugins): توسيع آفاق Dio
تُعد الإضافات (Plugins) بمثابة امتدادات سحرية تُضيف قدرات جديدة إلى Dio، مما يسمح لك بتخصيص سلوك الشبكة ليناسب احتياجات تطبيقك الفريدة. هذه الإضافات يمكن أن تُقدم وظائف مثل التخزين المؤقت (Caching)، أو إعادة المحاولة التلقائية (Automatic Retries)، أو حتى دمج Dio مع خدمات أخرى.
يمكنك استكشاف مجموعة واسعة من الإضافات الرسمية والمجتمعية على صفحة الإضافات في وثائق Dio.
دعوة للمساهمة: هل قمت بإنشاء إضافة رائعة لـ Dio؟ هل لديك مكتبة تستفيد من قدراتها؟ نرحب بك لتقديم إضافاتك ومكتباتك ذات الصلة من أطراف ثالثة هنا. شارك إبداعاتك مع المجتمع وكن جزءًا من هذا العالم المذهل!
أمثلة: رحلة عبر قدرات Dio
الآن، دعنا نغوص في أعماق الأمثلة العملية لنرى كيف يمكن لـ Dio أن تُنجز المهام الأكثر تعقيدًا بسهولة ويسر. هذه الأمثلة ستكون بمثابة دروس عملية تُظهر لك قوة ومرونة Dio في سيناريوهات مختلفة.
إجراء طلب GET: استكشاف البيانات
طلب GET هو الطلب الأساسي لجلب البيانات من الخادم. مع Dio، يمكنك إجراء هذا الطلب بطرق متعددة، سواء بتمرير المعلمات مباشرة في الرابط أو باستخدام كائن queryParameters.
import 'package:dio/dio.dart';
final dio = Dio();
void request() async {
Response response;
// الطريقة الأولى: تمرير المعلمات مباشرة في الرابط (URL)
response = await dio.get('/test?id=12&name=dio');
print('الاستجابة الأولى: ${response.data.toString()}');
// الطريقة الثانية: استخدام كائن queryParameters، وهي الطريقة المفضلة والأكثر تنظيمًا
// هذا الطلب هو نفسه الطلب أعلاه، لكنه أكثر وضوحًا وقابلية للصيانة.
response = await dio.get(
'/test',
queryParameters: {'id': 12, 'name': 'dio'},
);
print('الاستجابة الثانية: ${response.data.toString()}');
}
شرح مفصل:
dio.get('/test?id=12&name=dio'): هنا، يتم بناء الرابط يدوياً مع تضمين معلمات الاستعلام (Query Parameters) مباشرة. هذه الطريقة تعمل، ولكنها قد تصبح معقدة وصعبة القراءة عندما يكون هناك العديد من المعلمات أو عندما تحتاج إلى التعامل مع ترميز URL (URL Encoding).dio.get('/test', queryParameters: {'id': 12, 'name': 'dio'}): هذه هي الطريقة الموصى بها. تقوم Dio تلقائيًا بمعالجة كائنqueryParametersوتحويله إلى سلسلة استعلام (Query String) صحيحة ومُرمزة (Encoded)، مما يضمن التعامل السليم مع المسافات والأحرف الخاصة. هذا يجعل الكود أكثر نظافة، ووضوحًا، وأقل عرضة للأخطاء.
إجراء طلب POST: إرسال البيانات إلى المجهول
طلب POST يُستخدم لإرسال البيانات إلى الخادم، عادةً لإنشاء مورد جديد أو تحديث مورد موجود. مع Dio، يمكنك إرسال البيانات بسهولة باستخدام المعلمة data.
// مثال على إرسال بيانات JSON في طلب POST
response = await dio.post('/test', data: {'id': 12, 'name': 'dio'});
شرح مفصل:
dio.post('/test', data: {'id': 12, 'name': 'dio'}): هنا، يتم إرسال كائنMapكـdata. ستقوم Dio تلقائيًا بتحويل هذا الكائن إلى سلسلة JSON (JSON string) وتعيينContent-Typeالمناسب (عادةًapplication/json) في العناوين، مما يبسط عملية إرسال البيانات المنظمة إلى الخادم.
إجراء طلبات متعددة متزامنة: قوة التوازي
في بعض الأحيان، قد تحتاج إلى إجراء عدة طلبات HTTP في نفس الوقت لتحسين الأداء أو لجمع بيانات من مصادر مختلفة. توفر Dart و Dio طريقة أنيقة للتعامل مع هذا السيناريو باستخدام Future.wait.
// إجراء طلبين (POST و GET) في نفس الوقت والانتظار حتى يكتمل كلاهما
List<Response> responses = await Future.wait([
dio.post('/info'), // الطلب الأول
dio.get('/token') // الطلب الثاني
]);
// يمكنك الآن الوصول إلى استجابة كل طلب على حدة
print('استجابة طلب POST: ${responses[0].data}');
print('استجابة طلب GET: ${responses[1].data}');
شرح مفصل:
Future.wait([...]): هذه الدالة من مكتبةdart:asyncتأخذ قائمة من الكائناتFutureوتنتظر حتى تكتمل جميعها. بمجرد اكتمال جميع الـFuture، تُرجعFuture.waitقائمة تحتوي على نتائج كلFutureبالترتيب الذي تم تمريرها به.- الكفاءة: بدلاً من إجراء الطلبات بشكل متسلسل (واحد تلو الآخر)، يسمح
Future.waitبتنفيذها بالتوازي، مما يقلل بشكل كبير من الوقت الإجمالي اللازم لجلب جميع البيانات.
تنزيل ملف: جلب الكنوز الرقمية
تُعد عملية تنزيل الملفات من المهام الشائعة في التطبيقات الحديثة. يوفر Dio دالة download مخصصة تجعل هذه العملية سهلة وفعالة، مع إمكانية تحديد مسار حفظ الملف.
import 'package:path_provider/path_provider.dart'; // حزمة للحصول على مسارات الدليل المؤقت
// ... داخل دالة async ...
response = await dio.download(
'https://pub.dev/', // الرابط الذي سيتم تنزيل الملف منه
(await getTemporaryDirectory()).path + 'pub.html', // المسار الكامل لحفظ الملف
);
print('تم تنزيل الملف بنجاح إلى: ${response.requestOptions.path}');
شرح مفصل:
dio.download(url, savePath): هذه الدالة تقوم بتنزيل المحتوى منurlوحفظه مباشرة فيsavePath. المعلمةsavePathيمكن أن تكون مسارًا مطلقًا للملف على الجهاز.getTemporaryDirectory(): هذه الدالة (من حزمةpath_provider) تُرجعDirectoryيشير إلى دليل مؤقت على نظام الملفات، وهو مكان مثالي لحفظ الملفات التي لا تحتاج إلى البقاء بشكل دائم.- التقدم: يمكن لـ Dio أيضًا تتبع تقدم التنزيل، وهو أمر بالغ الأهمية للملفات الكبيرة، وسنتطرق إلى ذلك لاحقًا.
الحصول على تدفق الاستجابة (Response Stream): تدفق البيانات بلا حدود
في بعض الحالات، قد لا ترغب في تحميل الاستجابة بأكملها في الذاكرة دفعة واحدة، خاصة عند التعامل مع ملفات كبيرة أو تدفقات بيانات مستمرة. يوفر Dio خيار الحصول على الاستجابة كـ Stream، مما يتيح لك معالجة البيانات قطعة قطعة.
final rs = await dio.get(
url,
options: Options(responseType: ResponseType.stream), // ضبط نوع الاستجابة إلى `stream`.
);
print(rs.data.stream); // تدفق الاستجابة (Stream) الذي يمكنك الاستماع إليه.
شرح مفصل:
options: Options(responseType: ResponseType.stream): هذا الخيار يخبر Dio بأننا نريد الاستجابة كـStream. بدلاً من الحصول على البيانات الكاملة فيresponse.data، سنحصل على كائنResponseBodyالذي يحتوي علىstream.rs.data.stream: هذا هو الـStream<List<int>>الذي يمكنك الاستماع إليه لمعالجة البايتات الواردة من الخادم بشكل تدريجي. هذا النمط مفيد جداً لتطبيقات البث المباشر أو معالجة الملفات الكبيرة دون استهلاك ذاكرة مفرط.
الحصول على الاستجابة بالبايتات (Bytes): الغوص في أعماق البيانات الخام
إذا كنت بحاجة إلى التعامل مع البيانات الخام للاستجابة كقائمة من البايتات (List
final rs = await Dio().get<List<int>>(
url,
options: Options(responseType: ResponseType.bytes), // ضبط نوع الاستجابة إلى `bytes`.
);
print(rs.data); // النوع: List<int>، يحتوي على جميع بايتات الاستجابة.
شرح مفصل:
options: Options(responseType: ResponseType.bytes): يوجه هذا الخيار Dio لتحميل الاستجابة بأكملها كقائمة من البايتات. سيكونrs.dataمن النوعList<int>، مما يمنحك وصولاً مباشراً إلى البيانات الثنائية الخام.
إرسال FormData: بناء الجسور للبيانات المعقدة
عندما تحتاج إلى إرسال بيانات معقدة إلى الخادم، مثل مزيج من النصوص والملفات، فإن FormData هو الحل الأمثل. يستخدم هذا التنسيق multipart/form-data، وهو المعيار لرفع الملفات عبر HTTP.
final formData = FormData.fromMap({
'name': 'dio', // حقل نصي عادي
'date': DateTime.now().toIso8601String(), // حقل نصي آخر (تاريخ ووقت)
// يمكنك إضافة ملفات هنا أيضاً، كما سنرى في المثال التالي
});
final response = await dio.post('/info', data: formData);
print('تم إرسال FormData بنجاح: ${response.data}');
شرح مفصل:
FormData.fromMap({...}): هذه الدالة تُنشئ كائنFormDataمنMapعادي. المفاتيح هي أسماء الحقول، والقيم هي البيانات المراد إرسالها. ستقوم Dio تلقائيًا بتهيئة هذه البيانات لتنسيقmultipart/form-data.dio.post('/info', data: formData): يتم تمرير كائنformDataمباشرة إلى المعلمةdataفي طلبPOST. ستقوم Dio بضبطContent-Typeالمناسب تلقائيًا.
رفع ملفات متعددة عبر FormData: إطلاق العنان لفيض البيانات
تُعد القدرة على رفع ملفات متعددة في طلب واحد ميزة قوية. يسهل Dio هذه العملية بشكل كبير، مما يتيح لك إرسال عدة ملفات مع بيانات نصية أخرى في نفس طلب FormData.
import 'package:dio/dio.dart';
import 'dart:io'; // لاستخدام File
// ... داخل دالة async ...
final formData = FormData.fromMap({
'name': 'dio',
'date': DateTime.now().toIso8601String(),
// رفع ملف واحد
'file': await MultipartFile.fromFile('./text.txt', filename: 'upload.txt'),
// رفع ملفات متعددة كقائمة
'files': [
await MultipartFile.fromFile('./text1.txt', filename: 'text1.txt'),
await MultipartFile.fromFile('./text2.txt', filename: 'text2.txt'),
]
});
final response = await dio.post('/info', data: formData);
print('تم رفع الملفات بنجاح: ${response.data}');
شرح مفصل:
MultipartFile.fromFile(filePath, filename: fileName): هذه الدالة تُنشئ كائنMultipartFileمن ملف موجود على نظام الملفات.filePathهو المسار إلى الملف، وfilenameهو الاسم الذي سيظهر به الملف على الخادم.- قائمة الملفات: يمكنك تمرير قائمة من كائنات
MultipartFileإلى مفتاح واحد فيFormData(مثلfilesفي المثال)، وستقوم Dio بمعالجتها كملفات متعددة.
الاستماع لتقدم الرفع: مراقبة رحلة البيانات
عند رفع ملفات كبيرة، من الضروري تزويد المستخدم بتغذية راجعة حول تقدم العملية. يوفر Dio وظيفة onSendProgress التي تسمح لك بمراقبة البايتات المرسلة من إجمالي البايتات.
final response = await dio.post(
'https://www.dtworkroom.com/doris/1/2.0.0/test',
data: {'aa': 'bb' * 22}, // بيانات عشوائية لتمثيل حمولة كبيرة
onSendProgress: (int sent, int total) {
// يتم استدعاء هذه الدالة بشكل متكرر أثناء عملية الرفع
final progress = (sent / total * 100).toStringAsFixed(2); // حساب النسبة المئوية
print('تقدم الرفع: $sent بايت من $total بايت ($progress%)');
},
);
print('تم الرفع بنجاح: ${response.data}');
شرح مفصل:
onSendProgress: (int sent, int total) { ... }: هذه هي دالة رد الاتصال (Callback Function) التي يتم استدعاؤها كلما تم إرسال جزء من البيانات. تُمرر لها قيمتان:sent(عدد البايتات المرسلة حتى الآن) وtotal(الحجم الكلي للبيانات المراد إرسالها).- يمكنك استخدام هذه القيم لتحديث شريط التقدم (Progress Bar) في واجهة المستخدم أو لعرض معلومات حول حالة الرفع.
إرسال بيانات ثنائية عبر Stream: تدفق البايتات ببراعة
في بعض السيناريوهات المتقدمة، قد تحتاج إلى إرسال بيانات ثنائية (Binary Data) مباشرة كـ Stream. يتيح لك Dio القيام بذلك، مما يوفر مرونة كبيرة في التعامل مع أنواع البيانات المختلفة.
import 'package:dio/dio.dart';
import 'dart:async'; // لاستخدام Stream
// ... داخل دالة async ...
// بيانات ثنائية بسيطة (قائمة من الأعداد الصحيحة)
final postData = <int>[0, 1, 2, 3, 4, 5, 6, 7, 8, 9];
await dio.post(
url,
// تحويل قائمة البيانات إلى Stream<List<int>>
data: Stream.fromIterable(postData.map((e) => [e])),
options: Options(
headers: {
// من الضروري ضبط 'content-length' عند إرسال Stream لمراقبة التقدم
Headers.contentLengthHeader: postData.length,
},
),
);
print('تم إرسال البيانات الثنائية عبر Stream بنجاح.');
شرح مفصل:
data: Stream.fromIterable(postData.map((e) => [e])): هنا، يتم تحويل قائمةpostDataإلىStreamمنList<int>. كل عنصر في الـStreamهو قائمة تحتوي على بايت واحد.Headers.contentLengthHeader: postData.length: ملاحظة هامة: عند إرسال البيانات كـStream، يجب عليك ضبط عنوانContent-Lengthيدوياً فيheadersإذا كنت ترغب في تتبع تقدم الإرسال باستخدامonSendProgress. هذا يخبر الخادم (و Dio) بالحجم الكلي للبيانات المتوقعة.
شاهد جميع أكواد الأمثلة هنا في المستودع الأصلي للحصول على فهم أعمق وتطبيقات أكثر تنوعًا.
واجهات برمجة تطبيقات Dio (Dio APIs): قلب Dio النابض
الآن بعد أن استعرضنا بعض الأمثلة العملية، حان الوقت للغوص في قلب Dio النابض: واجهات برمجة التطبيقات (APIs) الخاصة بها. هذه الواجهات هي الأدوات الأساسية التي ستستخدمها لبناء طلبات HTTP قوية ومرنة.
إنشاء مثيل وضبط التكوينات الافتراضية: صياغة أداتك
يُعد إنشاء مثيل (Instance) من Dio هو الخطوة الأولى في أي تفاعل شبكي. يمكنك إنشاء مثيل بسيط، أو تخصيصه باستخدام كائن BaseOptions لضبط التكوينات الافتراضية التي ستُطبق على جميع الطلبات الصادرة من هذا المثيل.
نصيحة ذهبية: يُنصح بشدة باستخدام نمط المثيل الوحيد (Singleton) لـ
Dioفي مشاريعك. هذا النمط يضمن أن يكون لديك مثيل واحد فقط منDioفي جميع أنحاء تطبيقك، مما يتيح لك إدارة التكوينات مثل العناوين (headers)، والروابط الأساسية (base URLs)، والمهل الزمنية (timeouts) بشكل متسق ومركزي. هذا يقلل من التعقيد ويمنع التضارب في الإعدادات. إليك مثال يوضح كيفية استخدام Singleton في Flutter.
يمكنك إنشاء مثيل من Dio مع كائن BaseOptions اختياري:
import 'package:dio/dio.dart';
// إنشاء مثيل Dio باستخدام الخيارات الافتراضية تمامًا.
final dio = Dio();
void configureDio() {
// 1. ضبط التكوينات الافتراضية مباشرة على مثيل Dio الحالي
// هذه الإعدادات ستُطبق على جميع الطلبات التي تتم عبر هذا المثيل.
dio.options.baseUrl = 'https://api.pub.dev'; // الرابط الأساسي لجميع الطلبات
dio.options.connectTimeout = Duration(seconds: 5); // مهلة الاتصال بالخادم (5 ثوانٍ)
dio.options.receiveTimeout = Duration(seconds: 3); // مهلة استقبال البيانات (3 ثوانٍ)
// 2. أو إنشاء مثيل `Dio` جديد تمامًا مع كائن `BaseOptions` مخصص
// هذه الطريقة مفيدة إذا كنت بحاجة إلى مثيلات Dio متعددة بتكوينات مختلفة.
final options = BaseOptions(
baseUrl: 'https://api.pub.dev',
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 3),
// يمكنك إضافة المزيد من الخيارات هنا مثل headers, contentType, إلخ.
);
final anotherDio = Dio(options);
// 3. أو استنساخ (Clone) مثيل `Dio` موجود مع جميع حقوله
// هذا مفيد إذا كنت تريد إنشاء مثيل جديد بناءً على تكوينات مثيل موجود، ثم تعديل بعضها.
final clonedDio = dio.clone();
// يمكنك الآن تعديل خصائص clonedDio دون التأثير على dio الأصلي
clonedDio.options.baseUrl = 'https://another.api.com';
}
شرح مفصل:
final dio = Dio();: هذا يُنشئ مثيلاً بسيطاً منDioباستخدام الإعدادات الافتراضية. يمكنك لاحقاً تعديل هذه الإعدادات عبرdio.options.dio.options.baseUrl = '...': يحدد هذا الرابط الأساسي الذي سيتم إلحاق المسارات النسبية (Relative Paths) به. على سبيل المثال، إذا كانbaseUrlهوhttps://api.pub.devوقمت بطلبdio.get('/packages')، فسيكون الرابط الكامل هوhttps://api.pub.dev/packages.connectTimeout: هي المدة القصوى التي يجب أن ينتظرها Dio لإنشاء اتصال بالخادم. إذا تجاوزت هذه المدة، سيتم إطلاقDioExceptionType.connectTimeout.receiveTimeout: هي المدة القصوى التي يجب أن ينتظرها Dio لاستقبال البيانات بعد إنشاء الاتصال. هذا لا يشمل وقت الاتصال، بل وقت استقبال البيانات فقط. إذا تجاوزت هذه المدة، سيتم إطلاقDioExceptionType.receiveTimeout.Dio(options): يسمح لك بتمرير كائنBaseOptionsمخصص عند إنشاء المثيل، مما يمنحك تحكماً كاملاً في التكوينات الأولية.dio.clone(): يُنشئ نسخة طبق الأصل من مثيلDioالحالي، بما في ذلك جميع خياراته ومراقبيه. هذا مفيد عندما تحتاج إلى مثيلDioجديد بنفس الإعدادات ولكن مع بعض التعديلات الطفيفة دون التأثير على المثيل الأصلي.
الواجهة الأساسية في مثيل Dio، والتي تُعد العمود الفقري لجميع طلبات HTTP، هي دالة request:
Future<Response<T>> request<T>(
String path, { // المسار النسبي أو المطلق للطلب
Object? data, // البيانات المراد إرسالها مع الطلب (لـ POST, PUT, PATCH)
Map<String, dynamic>? queryParameters, // معلمات الاستعلام (Query Parameters) لـ GET
CancelToken? cancelToken, // رمز الإلغاء لإلغاء الطلب لاحقًا
Options? options, // خيارات الطلب المحددة لهذا الطلب فقط
ProgressCallback? onSendProgress, // دالة رد اتصال لتتبع تقدم الإرسال
ProgressCallback? onReceiveProgress, // دالة رد اتصال لتتبع تقدم الاستقبال
});
هذه الدالة هي الأكثر مرونة وقوة، حيث تسمح لك بتحديد جميع جوانب الطلب. يمكنك استخدامها لإجراء أي نوع من طلبات HTTP (GET, POST, PUT, DELETE, إلخ) عن طريق تحديد method في كائن Options.
مثال على استخدام دالة request:
final response = await dio.request(
'/test',
data: {'id': 12, 'name': 'dio'},
options: Options(method: 'GET'), // تحديد طريقة الطلب كـ GET
);
print('استجابة طلب request: ${response.data}');
شرح مفصل:
dio.request('/test', ...): هذه هي الدالة العامة لإجراء الطلبات. يمكنك تحديد المسار، البيانات، ومعلمات الاستعلام، بالإضافة إلى خيارات محددة للطلب.options: Options(method: 'GET'): هنا، نُحدد أن طريقة الطلب هيGET، على الرغم من أننا نستخدمdata. هذا يوضح مرونة دالةrequestفي التعامل مع سيناريوهات مختلفة.
خيارات الطلب (Request Options): توجيه سفينتك
في مكتبة Dio، هناك مفهومان أساسيان لخيارات الطلب يمنحانك تحكماً دقيقاً في كيفية إرسال الطلبات واستقبال الاستجابات:
BaseOptions: هذه هي الإعدادات الأساسية التي تُطبق على مثيلDio()بأكمله. إنها بمثابة الدفة الرئيسية لسفينتك، حيث تحدد الاتجاه العام لجميع الرحلات (الطلبات) التي تقوم بها.Options: هذه هي الإعدادات المحددة لطلب واحد فقط. إنها بمثابة التعديلات الدقيقة التي تُجريها على مسار سفينتك لرحلة معينة، دون التأثير على الاتجاه العام. سيتم دمج هذه الخيارات معBaseOptionsعند إجراء الطلب، حيث تُعطى الأولوية لـOptionsالمحددة للطلب الفردي.
إعلان Options يكون كما يلي، وهو يكشف عن ثراء الخيارات المتاحة لتخصيص كل طلب:
/// طريقة طلب HTTP (Method).
/// تحدد نوع العملية التي سيتم إجراؤها على المورد (GET, POST, PUT, DELETE, إلخ).
String method;
/// المهلة الزمنية عند إرسال البيانات (Send Timeout).
///
/// هذه المدة تحدد أقصى وقت مسموح به لإرسال بيانات الطلب بالكامل إلى الخادم.
/// إذا تجاوزت عملية الإرسال هذه المدة، سيتم إطلاق [DioException] من نوع
/// [DioExceptionType.sendTimeout].
///
/// القيمة `null` أو `Duration.zero` تعني عدم وجود حد للمهلة، مما قد يؤدي إلى تعليق التطبيق في حال وجود مشاكل في الشبكة أو الخادم.
Duration? sendTimeout;
/// المهلة الزمنية عند استقبال البيانات (Receive Timeout).
///
/// هذه المدة تمثل:
/// - مهلة قبل إنشاء الاتصال واستقبال أول بايتات الاستجابة.
/// - المدة أثناء نقل البيانات لكل حدث بايت، وليس المدة الإجمالية للاستقبال.
///
/// بعبارة أخرى، إذا توقف تدفق البيانات لأكثر من هذه المدة بين أي بايتين متتاليين، فسيتم اعتبار ذلك مهلة.
/// إذا تجاوزت هذه المدة، سيتم إطلاق [DioException] من نوع
/// [DioExceptionType.receiveTimeout].
///
/// القيمة `null` أو `Duration.zero` تعني عدم وجود حد للمهلة.
Duration? receiveTimeout;
/// حقل مخصص (Extra) يمكنك استعادته لاحقًا.
///
/// هذا الحقل يسمح لك بتخزين أي بيانات إضافية تحتاجها للتعامل مع الطلب في المراحل المختلفة،
/// مثل داخل [Interceptor]، أو [Transformer]، أو حتى في كائن [Response.requestOptions].
/// إنه بمثابة حقيبة سفر صغيرة يمكنك وضع فيها ملاحظاتك الخاصة بالرحلة.
Map<String, dynamic>? extra;
/// عناوين طلب HTTP (Headers).
///
/// هذه هي المعلومات الإضافية التي تُرسل مع الطلب، مثل نوع المحتوى (Content-Type)،
/// أو رمز المصادقة (Authorization Token)، أو وكيل المستخدم (User-Agent).
///
/// مفاتيح العناوين غير حساسة لحالة الأحرف،
/// مثال: `content-type` و `Content-Type` سيتم عاملهما كالمفتاح نفسه.
Map<String, dynamic>? headers;
/// ما إذا كان يجب الحفاظ على حالة أحرف مفاتيح العناوين (preserveHeaderCase).
///
/// القيمة الافتراضية هي `false`.
///
/// هذا الخيار **لن يعمل** في هذه الحالات:
/// - XHR ([HttpRequest]) لا يدعم التعامل مع هذا صراحة في بيئات المتصفح.
/// - معيار HTTP/2 يدعم فقط مفاتيح العناوين الصغيرة (lowercase)، لذا سيتم تحويلها تلقائيًا.
bool? preserveHeaderCase;
/// نوع البيانات التي يتعامل معها [Dio] مع الخيارات (ResponseType).
///
/// يحدد هذا الخيار كيفية تفسير Dio لجسم الاستجابة الوارد من الخادم.
///
/// القيمة الافتراضية هي [ResponseType.json].
/// سيقوم [Dio] بتحليل سلسلة الاستجابة إلى كائن JSON تلقائيًا
/// عندما يكون نوع المحتوى (content-type) للاستجابة هو [Headers.jsonContentType].
///
/// انظر أيضًا:
/// - `plain` إذا كنت تريد استقبال البيانات كـ `String` (نص عادي).
/// - `bytes` إذا كنت تريد استقبال البيانات كبايتات كاملة (`List<int>`)، وهو مفيد للملفات الثنائية.
/// - `stream` إذا كنت تريد استقبال البيانات كـ `Stream` لمعالجتها بشكل تدريجي.
String? contentType;
/// يحدد ما إذا كان الطلب يعتبر ناجحًا بناءً على رمز الحالة (Status Code).
///
/// هذه الدالة تسمح لك بتحديد منطق مخصص لتحديد ما إذا كانت الاستجابة ناجحة أم لا.
/// سيتم التعامل مع الطلب على أنه ناجح إذا أعادت دالة رد الاتصال (callback) القيمة `true`.
ValidateStatus? validateStatus;
/// ما إذا كان يجب استرداد البيانات عندما يشير رمز الحالة إلى طلب فاشل (receiveDataWhenStatusError).
///
/// القيمة الافتراضية هي `true`.
/// حتى لو كان رمز الحالة يشير إلى خطأ (مثل 404 أو 500)، فإن Dio سيحاول استرداد جسم الاستجابة.
/// إذا قمت بتعيينها إلى `false`، فلن يتم استرداد جسم الاستجابة في حالة الأخطاء.
bool? receiveDataWhenStatusError;
/// يتبع إعادة التوجيه (followRedirects).
///
/// يحدد ما إذا كان Dio يجب أن يتبع تلقائيًا عمليات إعادة التوجيه التي يرسلها الخادم.
///
/// القيمة الافتراضية هي `true`.
bool? followRedirects;
/// الحد الأقصى لعدد عمليات إعادة التوجيه (maxRedirects).
///
/// إذا كانت `followRedirects` هي `true`، فإن هذا يحدد الحد الأقصى لعدد مرات إعادة التوجيه المسموح بها.
/// سيتم إطلاق [RedirectException] إذا تجاوزت عمليات إعادة التوجيه هذا الحد.
///
/// القيمة الافتراضية هي `5`.
int? maxRedirects;
/// اتصال دائم (persistentConnection).
///
/// يحدد ما إذا كان الاتصال بالخادم يجب أن يظل مفتوحًا لإعادة استخدامها في الطلبات اللاحقة.
///
/// القيمة الافتراضية هي `true`.
bool? persistentConnection;
/// مُرمز الطلب (RequestEncoder).
///
/// المرمز الافتراضي للطلب هو [Utf8Encoder]. يمكنك تعيين مُرمز مخصص
/// باستخدام هذا الخيار لتحويل بيانات الطلب إلى بايتات قبل الإرسال.
RequestEncoder? requestEncoder;
/// مُفكك ترميز الاستجابة (ResponseDecoder).
///
/// مُفكك الترميز الافتراضي للاستجابة هو [Utf8Decoder]. يمكنك تعيين مُفكك ترميز مخصص
/// باستخدام هذا الخيار، وسيتم استخدامه في [Transformer] لتحويل بايتات الاستجابة إلى بيانات قابلة للقراءة.
ResponseDecoder? responseDecoder;
/// يحدد تنسيق بيانات المجموعة (Collection Data) في معلمات استعلام الطلب (Query Parameters)
/// وبيانات جسم `x-www-url-encoded`.
///
/// القيمة الافتراضية هي [ListFormat.multi].
ListFormat? listFormat;
هناك مثال كامل يوضح كيفية استخدام هذه الخيارات المتنوعة هنا في المستودع الأصلي. استكشفه لتفهم كيف يمكنك توجيه سفينتك (طلباتك) بدقة في بحر الشبكة.
هناك مثال كامل يوضح كيفية استخدام هذه الخيارات المتنوعة هنا في المستودع الأصلي. استكشفه لتفهم كيف يمكنك توجيه سفينتك (طلباتك) بدقة في بحر الشبكة.
الاستجابة (Response): حصاد رحلتك
بعد أن تُرسل طلبك إلى الخادم، ستتلقى استجابة (Response) تحمل في طياتها نتيجة هذه الرحلة. كائن الاستجابة في Dio هو بمثابة كنز من المعلومات، حيث يحتوي على كل ما تحتاج لمعرفته حول ما عاد به الخادم إليك. إليك تفصيل لمكونات هذا الكائن:
/// جسم الاستجابة (Response body).
/// هذا هو المحتوى الفعلي الذي أرسله الخادم، وقد يكون قد تم تحويله بالفعل بواسطة Dio
/// بناءً على [ResponseType] الذي حددته (مثل JSON، نص عادي، أو بايتات).
T? data;
/// معلومات الطلب المقابل (RequestOptions).
/// هذا الكائن يحتوي على جميع الخيارات التي تم استخدامها لإجراء الطلب الأصلي.
/// إنه مفيد جداً لتتبع سياق الطلب الذي أدى إلى هذه الاستجابة.
RequestOptions requestOptions;
/// رمز حالة HTTP (HTTP Status Code).
/// رقم يشير إلى نتيجة الطلب (مثال: 200 للنجاح، 404 للمورد غير موجود، 500 لخطأ في الخادم).
int? statusCode;
/// رسالة السبب (Status Message) المرتبطة برمز الحالة.
/// نص وصفي موجز لرمز الحالة (مثال: "OK" لـ 200، "Not Found" لـ 404).
/// يجب ضبط رسالة السبب قبل كتابة جسم الاستجابة.
String? statusMessage;
/// ما إذا كانت هذه الاستجابة عبارة عن إعادة توجيه (isRedirect).
/// قيمة منطقية (true/false) تشير إلى ما إذا كان الخادم قد طلب إعادة توجيه الطلب إلى عنوان URL آخر.
/// ** تنبيه **: توفر هذا الحقل يعتمد على ما إذا كان تنفيذ المحول (adapter) يدعمه أم لا.
bool isRedirect;
/// سلسلة عمليات إعادة التوجيه (Redirect Records).
/// قائمة بسجلات إعادة التوجيه التي مر بها هذا الاتصال. ستكون القائمة فارغة إذا لم يتم اتباع أي عمليات إعادة توجيه.
/// يتم تحديث [redirects] في كل من حالات إعادة التوجيه التلقائية واليدوية.
///
/// ** تنبيه **: توفر هذا الحقل يعتمد على ما إذا كان تنفيذ المحول (adapter) يدعمه أم لا.
List<RedirectRecord> redirects;
/// حقول مخصصة (Extra) فقط للاستجابة [Response].
/// هذا الحقل يسمح لك بتخزين أي بيانات إضافية خاصة بالاستجابة،
/// والتي قد تكون مفيدة للمعالجة اللاحقة.
Map<String, dynamic> extra;
/// عناوين الاستجابة (Response Headers).
/// هذه هي المعلومات الإضافية التي أرسلها الخادم مع الاستجابة،
/// مثل نوع المحتوى (Content-Type)، طول المحتوى (Content-Length)، أو الكوكيز (Cookies).
Headers headers;
عندما يكلل طلبك بالنجاح، ستتلقى الاستجابة التي تحمل البيانات التي طلبتها. إليك كيف يمكنك الوصول إلى هذه المعلومات:
final response = await dio.get("https://pub.dev");
print("البيانات المستلمة: ${response.data}"); // جسم الاستجابة الفعلي
print("عناوين الاستجابة: ${response.headers}"); // عناوين HTTP التي أرسلها الخادم
print("خيارات الطلب المستخدمة: ${response.requestOptions}"); // الخيارات التي تم بها الطلب
print("رمز حالة HTTP: ${response.statusCode}"); // رمز الحالة (مثال: 200 OK)
نقطة هامة يجب الانتباه إليها: Response.extra يختلف تمامًا عن RequestOptions.extra. على الرغم من تشابه الاسم، إلا أنهما حقلان منفصلان تمامًا وغير مرتبطين ببعضهما البعض. RequestOptions.extra يُستخدم لتخزين بيانات إضافية للطلب قبل إرساله، بينما Response.extra يُستخدم لتخزين بيانات إضافية خاصة بالاستجابة بعد استلامها.
المراقبون (Interceptors): حراس البوابة الأوفياء
تُعد المراقبون (Interceptors) من أقوى الميزات في Dio، فهي تمنحك القدرة على التدخل في دورة حياة طلب HTTP بأكملها. تخيل أن لديك حراس بوابة يقفون عند كل نقطة عبور (إرسال الطلب، استقبال الاستجابة، حدوث خطأ)، ويمكنهم فحص، تعديل، أو حتى منع مرور البيانات. هذا يسمح لك بتطبيق منطق عام (Global Logic) على جميع الطلبات دون الحاجة إلى تكرار الكود في كل مكان.
لكل مثيل من Dio، يمكنك إضافة واحد أو أكثر من المراقبين. من خلالهم، يمكننا اعتراض الطلبات، والاستجابات، والأخطاء قبل أن يتم التعامل معها بواسطة then (للنجاح) أو catchError (للأخطاء).
dio.interceptors.add(
InterceptorsWrapper(
// دالة تُستدعى قبل إرسال الطلب
onRequest: (RequestOptions options, RequestInterceptorHandler handler) {
// هنا يمكنك فحص أو تعديل الـ `options` قبل إرسال الطلب.
// على سبيل المثال، إضافة رمز مصادقة (Authorization Token) إلى العناوين.
print("-> [${options.method}] ${options.uri}");
// إذا أردت حل الطلب ببيانات مخصصة (دون إرساله فعليًا)،
// يمكنك استدعاء `handler.resolve(response)`.
// إذا أردت رفض الطلب برسالة خطأ (منعه من الإرسال)،
// يمكنك استدعاء `handler.reject(dioError)`.
return handler.next(options); // استمر في معالجة الطلب
},
// دالة تُستدعى بعد استقبال الاستجابة بنجاح
onResponse: (Response response, ResponseInterceptorHandler handler) {
// هنا يمكنك فحص أو تعديل الـ `response` قبل أن تصل إلى الكود الخاص بك.
// على سبيل المثال، فك تشفير البيانات أو معالجة الأخطاء الشائعة.
print("<- [${response.statusCode}] ${response.requestOptions.uri}");
// إذا أردت رفض الاستجابة برسالة خطأ،
// يمكنك استدعاء `handler.reject(dioError)`.
return handler.next(response); // استمر في معالجة الاستجابة
},
// دالة تُستدعى عند حدوث خطأ في الطلب أو الاستجابة
onError: (DioException error, ErrorInterceptorHandler handler) {
// هنا يمكنك التعامل مع الأخطاء بشكل مركزي.
// على سبيل المثال، تحديث رمز المصادقة (Refresh Token) أو إعادة محاولة الطلب.
print("!! [${error.response?.statusCode}] ${error.requestOptions.uri}");
// إذا أردت حل الطلب ببعض البيانات المخصصة (بعد حدوث الخطأ)،
// يمكنك استدعاء `handler.resolve(response)`.
return handler.next(error); // استمر في معالجة الخطأ
},
),
);
مثال بسيط لمراقب مخصص:
يمكنك إنشاء مراقب مخصص عن طريق توسيع الفئة Interceptor، مما يمنحك تحكماً أكبر في كيفية تسجيل الأحداث أو معالجتها.
import 'package:dio/dio.dart';
class CustomInterceptors extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
// طباعة معلومات الطلب قبل إرساله
print('REQUEST[${options.method}] => PATH: ${options.path}');
super.onRequest(options, handler); // استدعاء التنفيذ الأصلي للمراقب
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
// طباعة معلومات الاستجابة بعد استلامها بنجاح
print('RESPONSE[${response.statusCode}] => PATH: ${response.requestOptions.path}');
super.onResponse(response, handler); // استدعاء التنفيذ الأصلي للمراقب
}
@override
Future onError(DioException err, ErrorInterceptorHandler handler) async {
// طباعة معلومات الخطأ عند حدوثه
print('ERROR[${err.response?.statusCode}] => PATH: ${err.requestOptions.path}');
super.onError(err, handler); // استدعاء التنفيذ الأصلي للمراقب
}
}
حل ورفض الطلب (Resolve and reject the request): التحكم المطلق
في قلب كل مراقب، تكمن القدرة على التحكم المطلق في تدفق الطلب أو الاستجابة. يمكنك التدخل في مسار التنفيذ وتغيير النتيجة بشكل جذري:
handler.resolve(Response): إذا كنت تريد حل الطلب أو الاستجابة ببيانات مخصصة (دون إرسال الطلب فعليًا إلى الخادم أو تمرير الاستجابة الفعلية إلى الكود الخاص بك)، يمكنك استدعاء هذه الدالة. هذا مفيد لسيناريوهات التخزين المؤقت (Caching) حيث يمكنك إرجاع بيانات مخزنة محليًا بدلاً من إجراء طلب شبكة.handler.reject(dioError): إذا كنت تريد رفض الطلب أو الاستجابة برسالة خطأ، يمكنك استدعاء هذه الدالة. هذا يسمح لك بإيقاف معالجة الطلب أو الاستجابة وإطلاق استثناء مخصص، وهو مفيد للتحقق من الصحة (Validation) أو التعامل مع الأخطاء الأمنية.
مثال يوضح كيفية حل طلب ببيانات وهمية (Fake Data) مباشرة من المراقب:
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
// بدلاً من إرسال الطلب فعليًا، نقوم بحله ببيانات وهمية
return handler.resolve(
Response(requestOptions: options, data: 'fake data'),
);
},
),
);
final response = await dio.get('/test');
print(response.data); // سيتم طباعة 'fake data' بدلاً من بيانات الخادم
المراقب المجدول (QueuedInterceptor): التنفيذ المتسلسل بذكاء
بشكل افتراضي، يمكن تنفيذ المراقبين (Interceptors) بشكل متزامن، مما يعني أن جميع الطلبات قد تدخل المراقب في وقت واحد، بدلاً من التنفيذ المتسلسل. ومع ذلك، في بعض الحالات، قد تحتاج إلى أن تدخل الطلبات المراقب بالتتابع (خطوة بخطوة) لضمان معالجة صحيحة ومنظمة، مثلما هو موضح في المشكلة رقم #590. هنا يأتي دور QueuedInterceptor ليحل هذه المشكلة، حيث يضمن الوصول المتسلسل (step by step) إلى المراقبين.
مثال: حماية بياناتك بـ CSRF Token
لنفترض أنك تعمل على تطبيق يتطلب حماية قوية ضد هجمات تزوير الطلبات عبر المواقع (CSRF). لأسباب أمنية، تحتاج جميع الطلبات إلى تعيين csrfToken في العنوان (Header). إذا لم يكن csrfToken موجودًا، فإنك تحتاج إلى طلب csrfToken أولاً، ثم إجراء طلب الشبكة الأصلي. نظرًا لأن عملية طلب csrfToken غير متزامنة، فإنك تحتاج إلى تنفيذ هذا الطلب غير المتزامن داخل مراقب الطلب.
QueuedInterceptor يضمن أن يتم الحصول على csrfToken مرة واحدة فقط، وأن جميع الطلبات اللاحقة تنتظر حتى يتم الحصول عليه وتعيينه في العناوين قبل المتابعة. هذا يمنع حدوث "حالة سباق" (Race Condition) ويضمن أن جميع الطلبات محمية بشكل صحيح.
للكود الكامل الذي يوضح هذا السيناريو، انظر هنا.
مراقب السجلات (LogInterceptor): عيون Dio الساهرة
LogInterceptor هو مراقب جاهز للاستخدام يوفر لك رؤية عميقة لما يحدث داخل Dio. يمكنك تطبيقه لتسجيل تفاصيل الطلبات والاستجابات والأخطاء تلقائيًا في السجلات (Logs)، مما يجعله أداة لا غنى عنها لتصحيح الأخطاء (Debugging) ومراقبة سلوك الشبكة.
ملاحظة هامة: يجب أن يكون LogInterceptor دائمًا هو آخر مراقب يتم إضافته إلى قائمة المراقبين. السبب في ذلك هو أنه إذا تم إضافة مراقبين آخرين بعده، فإن أي تعديلات يقومون بها على الطلب أو الاستجابة لن يتم تسجيلها بواسطة LogInterceptor، مما قد يؤدي إلى معلومات غير دقيقة في السجلات.
Dart: تسجيل السجلات في بيئة Dart
في بيئة Dart النقية (بدون Flutter)، يمكنك استخدام LogInterceptor لتسجيل السجلات في وحدة التحكم (Console).
dio.interceptors.add(LogInterceptor(responseBody: false)); // عدم إخراج جسم الاستجابة في السجلات.
ملاحظة: عند استخدام دالة logPrint الافتراضية، سيتم طباعة السجلات فقط في وضع التصحيح (DEBUG mode) (عند تمكين assert). هذا يعني أن هذه السجلات لن تظهر في إصدارات الإنتاج (Release Builds) من تطبيقك، مما يحافظ على نظافة السجلات وأداء التطبيق.
بدلاً من ذلك، يمكن استخدام دالة log من مكتبة dart:developer لتسجيل الرسائل، وهي متاحة في Flutter أيضًا وتوفر ميزات أكثر تقدمًا لتسجيل السجلات.
Flutter: تسجيل السجلات في بيئة Flutter
عند العمل في بيئة Flutter، يُنصح بشدة باستخدام دالة debugPrint الخاصة بـ Flutter لتسجيل السجلات.
هذا يضمن أن رسائل التصحيح (Debug Messages) ستكون متاحة أيضًا عبر أمر flutter logs، مما يسهل عملية تصحيح الأخطاء في التطبيقات التي تعمل على الأجهزة الفعلية أو المحاكيات.
ملاحظة هامة: debugPrint لا تعني طباعة السجلات فقط في وضع DEBUG. إنها دالة مُحسّنة (throttled function) تساعد في طباعة السجلات الكاملة دون اقتطاع، حتى لو كانت طويلة جدًا. لا تستخدمها في أي بيئة إنتاج (Production Environment) إلا إذا كنت تقصد ذلك تمامًا، حيث أن طباعة السجلات بكثرة في الإنتاج قد تؤثر على الأداء وتكشف عن معلومات حساسة.
dio.interceptors.add(
LogInterceptor(
logPrint: (o) => debugPrint(o.toString()), // استخدام debugPrint لطباعة السجلات
),
);
مراقب مخصص (Custom Interceptor): إبداعك بلا حدود
إذا كانت المراقبون الجاهزون لا تلبي احتياجاتك الفريدة، يمكنك دائمًا إنشاء مراقب مخصص خاص بك عن طريق توسيع الفئة Interceptor أو QueuedInterceptor. هذا يفتح لك الأبواب لإمكانيات لا حصر لها لتخصيص سلوك الشبكة.
هناك مثال يوضح كيفية تنفيذ سياسة تخزين مؤقت بسيطة باستخدام مراقب مخصص: مراقب التخزين المؤقت المخصص. هذا المثال يُظهر كيف يمكنك التحكم في تدفق البيانات وتخزينها محليًا لتحسين الأداء وتقليل الاعتماد على الشبكة.
التعامل مع الأخطاء (Handling Errors): فن إدارة التحديات
في عالم الشبكات، الأخطاء حتمية. ولكن بدلاً من أن تكون عائقًا، يمكن أن تكون فرصة لتعزيز قوة تطبيقك ومرونته. Dio تُحول الأخطاء إلى فرص للتعلم والتحسين من خلال كائن DioException المخصص. عندما يحدث خطأ أثناء طلب HTTP، يقوم Dio بتغليف Error/Exception الأصلي في كائن DioException، مما يوفر لك معلومات غنية ومفصلة حول طبيعة الخطأ.
try {
// محاولة إجراء طلب إلى مورد غير موجود (سيؤدي إلى خطأ 404)
await dio.get('https://api.pub.dev/not-exist');
} on DioException catch (e) {
// هنا يتم اعتراض أي DioException يحدث
// التحقق مما إذا كانت الاستجابة موجودة (أي أن الخادم استجاب ولكن برمز خطأ)
if (e.response != null) {
print('خطأ في الاستجابة من الخادم:');
print('البيانات: ${e.response.data}'); // بيانات الخطأ التي أرسلها الخادم
print('العناوين: ${e.response.headers}'); // عناوين الاستجابة التي تحمل الخطأ
print('خيارات الطلب: ${e.response.requestOptions}'); // خيارات الطلب الذي أدى إلى الخطأ
} else {
// حدث شيء ما في إعداد أو إرسال الطلب لم يصل إلى الخادم
// (مثال: مشكلة في الاتصال بالإنترنت، خطأ DNS، مهلة اتصال)
print('خطأ في إعداد أو إرسال الطلب:');
print('خيارات الطلب: ${e.requestOptions}'); // خيارات الطلب الذي فشل
print('الرسالة: ${e.message}'); // رسالة الخطأ العامة
}
}
شرح مفصل:
on DioException catch (e): هذا هو النمط القياسي لالتقاط الأخطاء الخاصة بـ Dio. يسمح لك بالتعامل مع جميع الأخطاء المتعلقة بالشبكة بطريقة موحدة.e.response != null: هذا الشرط مهم جداً. إذا كانتe.responseغيرnull، فهذا يعني أن الطلب قد وصل إلى الخادم، والخادم قد استجاب، ولكن برمز حالة يشير إلى خطأ (مثل 400، 401، 403، 404، 500، إلخ). في هذه الحالة، يمكنك الوصول إلىdataوheadersوrequestOptionsمن كائن الاستجابة لمعرفة تفاصيل الخطأ من جانب الخادم.e.response == null: إذا كانتe.responseهيnull، فهذا يعني أن الطلب لم يتمكن حتى من الوصول إلى الخادم. قد يكون السبب مشكلة في الاتصال بالإنترنت، أو خطأ في اسم النطاق (DNS)، أو مهلة اتصال (Connection Timeout)، أو أي مشكلة أخرى تمنع الطلب من إكمال رحلته. في هذه الحالة، يمكنك الاعتماد علىe.requestOptionsوe.messageللحصول على معلومات حول الخطأ.
DioException: استثناءات Dio المخصصة
كائن DioException هو قلب نظام معالجة الأخطاء في Dio. إنه يوفر معلومات غنية ومفصلة حول الخطأ الذي حدث، مما يساعدك على تشخيص المشكلة بدقة واتخاذ الإجراءات المناسبة. إليك مكوناته الرئيسية:
/// معلومات الطلب (RequestOptions) الذي ألقى الاستثناء.
/// هذا يمنحك سياقًا كاملاً للطلب الذي فشل.
RequestOptions requestOptions;
/// معلومات الاستجابة (Response) التي أدت إلى الخطأ.
/// قد تكون `null` إذا لم يتمكن الطلب من الوصول إلى خادم HTTP،
/// على سبيل المثال، عند حدوث خطأ في DNS، أو عدم توفر الشبكة.
Response? response;
/// نوع [DioException] الحالي.
/// هذا الحقل يحدد الفئة العامة للخطأ (مثال: مهلة اتصال، خطأ في الاستجابة، إلخ).
DioExceptionType type;
/// كائن الخطأ/الاستثناء الأصلي (Original Error/Exception).
/// عادةً ما يكون غير `null` عندما يكون `type` هو [DioExceptionType.unknown].
/// هذا يمثل الخطأ الأساسي الذي تسبب في `DioException`.
Object? error;
/// تتبع المكدس (Stacktrace) لكائن الخطأ/الاستثناء الأصلي.
/// عادةً ما يكون غير `null` عندما يكون `type` هو [DioExceptionType.unknown].
/// يوفر هذا معلومات مفصلة عن تسلسل استدعاءات الدوال التي أدت إلى الخطأ.
StackTrace? stackTrace;
/// رسالة الخطأ (Error Message) التي ألقاها [DioException].
/// رسالة وصفية موجزة للخطأ.
String? message;
أنواع استثناءات ديو (DioExceptionType): مفتاح الفهم
لفهم أعمق لطبيعة الأخطاء التي قد تواجهها، يوفر Dio أنواعًا محددة من DioExceptionType. هذه الأنواع تساعدك على التمييز بين الأخطاء المختلفة وتطبيق منطق معالجة خاص لكل نوع. يمكنك مراجعة كود المصدر للحصول على القائمة الكاملة والوصف الدقيق لكل نوع.
بعض الأنواع الشائعة تشمل:
DioExceptionType.connectionTimeout: يحدث عندما لا يتمكن Dio من إنشاء اتصال بالخادم خلال المدة المحددة فيconnectTimeout.DioExceptionType.sendTimeout: يحدث عندما لا يتمكن Dio من إرسال بيانات الطلب بالكامل إلى الخادم خلال المدة المحددة فيsendTimeout.DioExceptionType.receiveTimeout: يحدث عندما لا يتمكن Dio من استقبال البيانات من الخادم خلال المدة المحددة فيreceiveTimeout.DioExceptionType.badResponse: يحدث عندما يستجيب الخادم برمز حالة يشير إلى خطأ (مثل 4xx أو 5xx).DioExceptionType.cancel: يحدث عندما يتم إلغاء الطلب يدويًا باستخدامCancelToken.DioExceptionType.unknown: يحدث لأي خطأ آخر غير مصنف، وقد يحتوي علىerrorوstackTraceالأصليين.
من خلال فهم هذه الأنواع، يمكنك بناء منطق معالجة أخطاء قوي وذكي في تطبيقك، مما يجعله أكثر مرونة وقدرة على التعافي من المشاكل الشبكية.
استخدام تنسيق application/x-www-form-urlencoded: البيانات التقليدية بأسلوب حديث
بشكل افتراضي، يقوم Dio بتسلسل بيانات الطلب (باستثناء نوع String) إلى تنسيق JSON، وهو التنسيق الأكثر شيوعًا ومرونة في تطبيقات الويب الحديثة. ومع ذلك، قد تواجه أحيانًا أنظمة خلفية (Backend Systems) أو واجهات برمجة تطبيقات (APIs) قديمة تتوقع البيانات بتنسيق application/x-www-form-urlencoded، وهو التنسيق التقليدي المستخدم في نماذج HTML.
لإرسال البيانات بهذا التنسيق بدلاً من JSON، يوفر Dio طرقًا سهلة لتحقيق ذلك:
// 1. ضبط Content-Type على مستوى مثيل Dio (Instance Level)
// هذا سيجعل جميع طلبات POST/PUT/PATCH الصادرة من هذا المثيل تستخدم تنسيق form-urlencoded
dio.options.contentType = Headers.formUrlEncodedContentType;
// 2. أو ضبط Content-Type لطلب واحد فقط (Works Once)
// هذه الطريقة مفيدة إذا كنت تحتاج إلى استخدام تنسيق form-urlencoded لطلب معين فقط
// دون التأثير على الإعدادات الافتراضية للمثيل.
dio.post(
'/info',
data: {'id': 5, 'name': 'John Doe'},
options: Options(contentType: Headers.formUrlEncodedContentType),
);
شرح مفصل:
Headers.formUrlEncodedContentType: هذا ثابت (Constant) معرف في Dio يمثل قيمةapplication/x-www-form-urlencoded. عند تعيينcontentTypeلهذا الثابت، ستقوم Dio تلقائيًا بتحويل كائنdata(مثلMap) إلى سلسلة نصية بتنسيقkey1=value1&key2=value2.- على مستوى المثيل (
dio.options.contentType = ...): هذا التكوين سيُطبق على جميع الطلبات اللاحقة التي تُرسل عبر هذا المثيل منDio. إنه مثالي إذا كان معظم أو كل طلباتك تتطلب هذا التنسيق. - على مستوى الطلب (
options: Options(contentType: ...)): هذا التكوين يُطبق فقط على الطلب الحالي. إنه يتجاوز أيcontentTypeافتراضي تم تعيينه فيdio.optionsلهذا الطلب المحدد. هذا يوفر مرونة كبيرة للتعامل مع متطلبات API مختلفة داخل نفس التطبيق.
إرسال FormData: قوة النماذج المتعددة الأجزاء
عندما يتعلق الأمر بإرسال بيانات معقدة إلى الخادم، مثل مزيج من الحقول النصية والملفات (صور، مستندات، فيديوهات)، فإن FormData هو الحل الأمثل. يستخدم هذا التنسيق multipart/form-data، وهو المعيار الذهبي لرفع الملفات عبر HTTP. Dio يجعل التعامل مع FormData سهلاً وقوياً.
import 'package:dio/dio.dart';
import 'dart:io'; // لاستخدام File و MultipartFile.fromFile
// ... داخل دالة async ...
final formData = FormData.fromMap({
'name': 'dio', // حقل نصي عادي
'date': DateTime.now().toIso8601String(), // حقل نصي آخر (تاريخ ووقت)
// إضافة ملف واحد: MultipartFile.fromFile ينشئ جزءًا من النموذج لملف
'file': await MultipartFile.fromFile('./text.txt', filename: 'upload.txt'),
});
// إرسال الـ FormData إلى الخادم باستخدام طلب POST
final response = await dio.post('/info', data: formData);
print('تم إرسال FormData بنجاح: ${response.data}');
شرح مفصل:
FormData.fromMap({...}): هذه الدالة البناءة (Constructor) تُنشئ كائنFormDataمنMapعادي. المفاتيح هي أسماء الحقول، والقيم هي البيانات المراد إرسالها. ستقوم Dio تلقائيًا بتهيئة هذه البيانات لتنسيقmultipart/form-data، مع إضافة الحدود (boundaries) اللازمة بين الأجزاء.MultipartFile.fromFile(filePath, filename: fileName): هذه الدالة الثابتة (Static Method) تُنشئ كائنMultipartFileمن ملف موجود على نظام الملفات.filePathهو المسار المطلق أو النسبي إلى الملف، وfilenameهو الاسم الذي سيظهر به الملف على الخادم. هذا الكائن يمثل جزءًا من النموذج الذي يحتوي على بيانات الملف.dio.post('/info', data: formData): يتم تمرير كائنformDataمباشرة إلى المعلمةdataفي طلبPOST. ستقوم Dio بضبطContent-Typeالمناسب تلقائيًا إلىmultipart/form-data.
يمكنك أيضًا تحديد اسم حدود (boundary name) مخصص لـ FormData، والذي سيُستخدم لبناء الحدود لكل FormData مع بادئة ولاحقة إضافية. هذا قد يكون مفيدًا في بعض السيناريوهات التي تتطلب توافقًا مع خوادم معينة:
final formDataWithBoundaryName = FormData(
boundaryName: 'my-custom-boundary-name',
);
// ثم يمكنك إضافة الحقول والملفات إلى formDataWithBoundaryName
ملاحظة هامة: يتم دعم
FormDataعادةً مع طريقةPOST، على الرغم من أنه يمكن استخدامه معPUTوPATCHفي بعض الحالات.
هناك مثال كامل يوضح استخدام FormData هنا.
رفع ملفات متعددة (Multiple files upload): إطلاق العنان لفيض البيانات
تُعد القدرة على رفع ملفات متعددة في طلب واحد ميزة قوية وضرورية في العديد من التطبيقات. يسهل Dio هذه العملية بشكل كبير، مما يتيح لك إرسال عدة ملفات مع بيانات نصية أخرى في نفس طلب FormData.
هناك طريقتان رئيسيتان لإضافة ملفات متعددة إلى FormData، والفرق الرئيسي بينهما يكمن في كيفية تمثيل مفاتيح الرفع (upload keys) لأنواع المصفوفات (Arrays) على جانب الخادم:
الطريقة الأولى: استخدام FormData.fromMap مع قائمة من MultipartFile
final formData = FormData.fromMap({
'files': [
MultipartFile.fromFileSync('path/to/upload1.txt', filename: 'upload1.txt'),
MultipartFile.fromFileSync('path/to/upload2.txt', filename: 'upload2.txt'),
],
});
شرح مفصل:
- في هذه الطريقة، يتم تمرير قائمة من كائنات
MultipartFileإلى مفتاح واحد (هناfiles). - ملاحظة هامة حول مفتاح الرفع: مفتاح الرفع في هذه الحالة سيصبح في النهاية
files[]على جانب الخادم. هذا يحدث لأن العديد من خدمات الخلفية (Backend Services) تضيف أقواسًا مربعة إلى المفتاح عندما تتلقى مصفوفة من الملفات. هذا السلوك شائع في العديد من أطر العمل (Frameworks) مثل PHP و Node.js (معmulter).
الطريقة الثانية: استخدام FormData مباشرة مع formData.files.addAll (إذا كنت لا تريد الأقواس المربعة)
إذا كنت لا تريد أن يتم إضافة الأقواس المربعة ([]) إلى مفتاح الرفع على جانب الخادم، أو إذا كان الخادم الخاص بك يتوقع تنسيقًا مختلفًا، فيجب عليك إنشاء FormData مباشرة وإضافة الملفات باستخدام formData.files.addAll:
final formData = FormData(); // إنشاء كائن FormData فارغ
formData.files.addAll([
MapEntry(
'files', // هنا يمكنك تحديد نفس المفتاح لكل ملف
MultipartFile.fromFileSync('./example/upload.txt',filename: 'upload.txt'),
),
MapEntry(
'files', // الملف الثاني بنفس المفتاح
MultipartFile.fromFileSync('./example/upload.txt',filename: 'upload.txt'),
),
]);
شرح مفصل:
- في هذه الطريقة، يتم إنشاء
FormDataفارغ، ثم يتم إضافةMapEntryلكل ملف. كلMapEntryيتكون من مفتاح (مثل'files') وقيمة (كائنMultipartFile). - هذه الطريقة تمنحك تحكمًا أكبر في كيفية تسمية مفاتيح الملفات، وتجنب إضافة
[]تلقائيًا إذا كان الخادم لا يتوقع ذلك.
إعادة استخدام FormData و MultipartFile: حكمة الاستخدام الأمثل
في رحلتك مع Dio، ستصادف مواقف قد تحتاج فيها إلى إرسال نفس FormData أو MultipartFile عدة مرات. ولكن هنا تكمن الحكمة: يجب عليك إنشاء FormData أو MultipartFile جديد في كل مرة تقوم فيها بإجراء طلبات متكررة.
لماذا؟
السلوك الخاطئ الشائع هو تعيين FormData كمتغير واستخدامه في كل طلب. هذا قد يؤدي بسهولة إلى استثناءات مثل "Cannot finalize" أو سلوك غير متوقع. السبب هو أن FormData و MultipartFile يستهلكان تدفقات البيانات (Streams) عند إرسالها. بمجرد قراءة التدفق، لا يمكن قراءته مرة أخرى. لذا، إذا حاولت إعادة استخدام نفس الكائن، فستكون تدفقات البيانات قد استُهلكت بالفعل، مما يؤدي إلى فشل الطلبات اللاحقة.
لتجنب هذه المشاكل، اتبع هذا النمط الحكيم في كتابة طلباتك:
Future<void> _repeatedlyRequest() async {
// دالة مساعدة لإنشاء FormData جديد في كل مرة
Future<FormData> createFormData() async {
return FormData.fromMap({
'name': 'dio',
'date': DateTime.now().toIso8601String(),
'file': await MultipartFile.fromFile('./text.txt',filename: 'upload.txt'),
});
}
// إجراء الطلب الأول بـ FormData جديد
await dio.post('some-url', data: await createFormData());
// إجراء الطلب الثاني بـ FormData جديد آخر
await dio.post('another-url', data: await createFormData());
// وهكذا...
}
شرح مفصل:
- يتم تعريف دالة
createFormData()التي تُرجعFuture<FormData>. في كل مرة يتم استدعاء هذه الدالة، يتم إنشاء كائنFormDataجديد تمامًا، بما في ذلك كائناتMultipartFileالجديدة. - عند إجراء كل طلب
dio.post، يتم استدعاءcreateFormData()لإنشاءFormDataجديد، مما يضمن أن كل طلب يستخدم تدفقات بيانات جديدة وغير مستهلكة. هذا يضمن استقرار وموثوقية تطبيقك عند التعامل مع رفع الملفات المتكرر.
المحول (Transformer): سحرة البيانات
في رحلة البيانات بين تطبيقك والخادم، قد تحتاج إلى تحويل هذه البيانات لتناسب احتياجات كل طرف. هنا يأتي دور Transformer، وهو ساحر البيانات الذي يسمح بإجراء تغييرات على بيانات الطلب/الاستجابة قبل إرسالها إلى الخادم أو بعد استقبالها منه.
يُقدم Dio افتراضيًا BackgroundTransformer، وهو محول ذكي يقوم باستدعاء jsonDecode في "عزلة" (Isolate) منفصلة إذا كانت الاستجابة أكبر من 50 كيلوبايت. هذا يضمن أن عملية تحليل JSON الكبيرة لا تُعيق واجهة المستخدم الرئيسية لتطبيقك، مما يحافظ على سلاسة التجربة.
إذا كنت ترغب في تخصيص عملية تحويل بيانات الطلب/الاستجابة، يمكنك توفير Transformer خاص بك واستبدال BackgroundTransformer الافتراضي عن طريق تعيينه لـ dio.transformer.
// مثال على تعيين محول مخصص
dio.transformer = MyCustomTransformer();
نقاط هامة حول المحولات: *
Transformer.transformRequest: يعمل هذا الجزء من المحول فقط عندما يتم إرسال الطلب باستخدام طرقPUT/POST/PATCH، وهي الطرق التي يمكن أن تحتوي على جسم الطلب (Request Body). *Transformer.transformResponse: على النقيض، يمكن تطبيق هذا الجزء من المحول على جميع أنواع الاستجابات، بغض النظر عن طريقة الطلب الأصلية.
مثال على المحول (Transformer example): تخصيص التحويل
هناك مثال كامل يوضح كيفية تخصيص المحول هنا. هذا المثال سيوضح لك كيف يمكنك بناء منطق تحويل خاص بك، سواء لتشفير البيانات قبل الإرسال، أو لفك تشفيرها بعد الاستقبال، أو لأي معالجة أخرى للبيانات.
محول عميل HTTP (HttpClientAdapter): الجسر إلى عالم HTTP
HttpClientAdapter هو بمثابة الجسر الحيوي الذي يربط بين Dio وعالم HTTP الحقيقي. Dio، بحد ذاتها، توفر واجهات برمجة تطبيقات (APIs) قياسية وسهلة الاستخدام للمطورين. ولكن في العمق، هناك كائن HttpClient هو الذي يقوم فعليًا بإجراء طلبات HTTP الحقيقية عبر الشبكة.
الفكرة هنا هي أننا لسنا مقيدين باستخدام HttpClient الافتراضي من مكتبة dart:io. يمكننا استخدام أي HttpClient آخر، أو حتى مكتبات HTTP مخصصة، طالما أننا نوفر HttpClientAdapter مناسبًا يعمل كـ "مترجم" بين Dio وهذا العميل. هذا يمنح Dio مرونة هائلة للتكيف مع بيئات مختلفة أو لدمج ميزات شبكات متقدمة.
المحول الافتراضي لـ Dio هو IOHttpClientAdapter على المنصات الأصلية (مثل Android و iOS)، و BrowserHttpClientAdapter على منصة الويب. يمكن تهيئة هذه المحولات ببساطة عن طريق استدعاء HttpClientAdapter():
dio.httpClientAdapter = HttpClientAdapter(); // يستخدم المحول الافتراضي للمنصة الحالية
إذا كنت ترغب في استخدام محولات المنصة صراحة، يمكنك القيام بذلك كما يلي:
-
لمنصة الويب:
dart import 'package:dio/browser.dart'; // ... dio.httpClientAdapter = BrowserHttpClientAdapter(); -
للمنصات الأصلية (Native Platforms):
dart import 'package:dio/io.dart'; // ... dio.httpClientAdapter = IOHttpClientAdapter();
هنا مثال بسيط يوضح كيفية تخصيص المحول، مما يفتح لك الباب لاستكشاف إمكانيات أعمق في التحكم بالاتصالات الشبكية.
استخدام البروكسي (Proxy): نافذتك إلى الشبكة
في بعض السيناريوهات، قد تحتاج إلى توجيه طلبات HTTP عبر خادم وكيل (Proxy Server)، سواء لأغراض الأمان، أو تجاوز القيود الجغرافية، أو تصحيح الأخطاء. يوفر IOHttpClientAdapter (للمنصات الأصلية) دالة رد اتصال createHttpClient تسمح لك بضبط إعدادات البروكسي لـ dart:io:HttpClient.
import 'package:dio/io.dart';
import 'dart:io'; // لاستخدام HttpClient
void initAdapter() {
dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
// هنا يمكنك تهيئة عميل HTTP (HttpClient) الخاص بك
client.findProxy = (uri) {
// توجيه جميع الطلبات إلى البروكسي
// 'localhost:8888'.
// انتبه، يجب أن يمر البروكسي عبر جهازك الذي يعمل عليه التطبيق،
// وليس منصة الاستضافة (Host Platform).
return 'PROXY localhost:8888';
};
// يمكنك أيضًا إنشاء HttpClient جديد لـ Dio بدلاً من إرجاع العميل الافتراضي،
// ولكن يجب إرجاع عميل هنا.
return client;
},
); }
هناك مثال كامل يوضح كيفية استخدام البروكسي [هنا](../example_dart/lib/proxy.dart).
> **ملاحظة هامة:** منصة الويب (Web) لا تدعم تعيين البروكسي بهذه الطريقة.
### التحقق من شهادة HTTPS: حصن الأمان الرقمي
يُشير التحقق من شهادة HTTPS (أو تثبيت المفتاح العام - Public Key Pinning) إلى عملية التأكد من أن الشهادات التي تحمي اتصال TLS بالخادم هي بالضبط تلك التي تتوقعها. الهدف من ذلك هو تقليل فرصة هجمات الوسيط (Man-in-the-Middle Attack). تم تغطية النظرية بالتفصيل بواسطة [OWASP](https://owasp.org/www-community/controls/Certificate_and_Public_Key_Pinning).
#### شهادة استجابة الخادم (Server Response Certificate)
على عكس الطرق الأخرى، تعمل هذه الطريقة مع شهادة الخادم نفسها مباشرةً.
```dart
import 'package:dio/io.dart';
import 'dart:io'; // لاستخدام HttpClient و SecurityContext
import 'package:crypto/crypto.dart'; // لتوليد SHA256
void initAdapter() {
// البصمة المتوقعة لشهادة الخادم (SHA256 Fingerprint)
const String fingerprint = 'ee5ce1dfa7a53657c545c62b65802e4272878dabd65c0aadcf85783ebb0b4d5c';
dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
// لا تثق بأي شهادة لمجرد أن شهادتها الجذرية موثوقة.
// نقوم بإنشاء HttpClient مع SecurityContext لا يثق بأي جذور موثوقة افتراضياً.
final HttpClient client = HttpClient(context: SecurityContext(withTrustedRoots: false));
// يمكنك اختبار الشهادة الوسيطة / الجذرية هنا. نحن نتجاهلها فقط في هذا المثال.
// هذا يعني أننا نقبل أي شهادة سيئة مبدئياً، ولكننا سنتحقق منها لاحقاً في validateCertificate.
client.badCertificateCallback = (cert, host, port) => true;
return client;
},
validateCertificate: (cert, host, port) {
// التحقق من أن بصمة الشهادة تتطابق مع البصمة التي نتوقعها.
// نحن بالتأكيد نطلب وجود شهادة ما.
if (cert == null) {
return false;
}
// تحقق منها بأي طريقة تريدها. هنا نتحقق فقط من أن
// البصمة تتطابق مع SHA256 من OpenSSL.
return fingerprint == sha256.convert(cert.der).toString();
},
);
}
شرح مفصل:
fingerprint: هذا هو المفتاح السحري! إنه بصمة SHA256 لشهادة الخادم التي تتوقعها. يجب أن تحصل على هذه البصمة من مصدر موثوق به (مثل مسؤول الخادم أو من خلال فحص الشهادة يدوياً).SecurityContext(withTrustedRoots: false): عند إنشاءHttpClient، نحددwithTrustedRoots: false، مما يعني أن العميل لن يثق تلقائياً بأي شهادات جذرية (Root Certificates) مثبتة في النظام. هذا يزيد من الأمان.client.badCertificateCallback = (cert, host, port) => true;: هذه الدالة تُستدعى عندما يواجه العميل شهادة غير صالحة. في هذا المثال، نُرجعtrueمؤقتاً، مما يعني أننا نقبل الشهادة "السيئة" في هذه المرحلة، ولكننا سنقوم بالتحقق الفعلي فيvalidateCertificate.validateCertificate: (cert, host, port) { ... }: هذه هي الدالة الأساسية للتحقق. هنا، نتحقق من أن الشهادة ليستnull، ثم نقوم بحساب بصمة SHA256 للشهادة المستلمة (cert.der) ومقارنتها بالبصمة المتوقعة (fingerprint). إذا تطابقتا، فإن الشهادة موثوقة.
يمكنك استخدام openssl لقراءة قيمة SHA256 لشهادة:
openssl s_client -servername pinning-test.badssl.com -connect pinning-test.badssl.com:443 < /dev/null 2>/dev/null \
| openssl x509 -noout -fingerprint -sha256
# SHA256 Fingerprint=EE:5C:E1:DF:A7:A5:36:57:C5:45:C6:2B:65:80:2E:42:72:87:8D:AB:D6:5C:0A:AD:CF:85:78:3E:BB:0B:4D:5C
# (قم بإزالة التنسيق، واحتفظ فقط بأحرف سداسية عشرية صغيرة لتتطابق مع sha256 أعلاه)
التحقق من سلطة الشهادة (Certificate Authority Verification)
تعمل هذه الطرق بشكل جيد عندما يكون لدى الخادم شهادة موقعة ذاتيًا (Self-Signed Certificate)، ولكنها لا تعمل مع الشهادات الصادرة عن طرف ثالث مثل AWS أو Let's Encrypt.
هناك طريقتان للتحقق من جذر سلسلة شهادات HTTPS التي يوفرها الخادم. بافتراض أن تنسيق الشهادة هو PEM، يكون الكود كالتالي:
import 'package:dio/io.dart';
import 'dart:io'; // لاستخدام HttpClient و X509Certificate
void initAdapter() {
String PEM = 'XXXXX'; // محتوى الشهادة الجذرية (root certificate content)
dio.httpClientAdapter = IOHttpClientAdapter(
createHttpClient: () {
final client = HttpClient();
client.badCertificateCallback = (X509Certificate cert, String host, int port) {
return cert.pem == PEM; // التحقق من الشهادة.
};
return client;
},
);
}
شرح مفصل:
- في هذه الطريقة، نقوم بتوفير محتوى الشهادة الجذرية (Root Certificate) بتنسيق PEM كسلسلة نصية (
PEM). - داخل
badCertificateCallback، نقوم بمقارنة محتوى PEM للشهادة المستلمة (cert.pem) مع محتوى PEM المتوقع. إذا تطابقا، فإننا نعتبر الشهادة صالحة.
طريقة أخرى هي إنشاء SecurityContext عند إنشاء HttpClient:
import 'package:dio/io.dart';
import 'dart:io'; // لاستخدام HttpClient و SecurityContext و File
void initAdapter() {
String PEM = 'XXXXX'; // محتوى الشهادة الجذرية (root certificate content)
dio.httpClientAdapter = IOHttpClientAdapter(
onHttpClientCreate: (_) {
final SecurityContext sc = SecurityContext();
// إضافة الشهادة الموثوقة من ملف.
sc.setTrustedCertificates(File(pathToTheCertificate));
final HttpClient client = HttpClient(context: sc);
return client;
},
);
}
شرح مفصل:
- هنا، نقوم بإنشاء
SecurityContextجديد. - نستخدم
sc.setTrustedCertificates(File(pathToTheCertificate))لإضافة شهادة جذر موثوقة من ملف محلي. هذا يعني أن أي شهادة يقدمها الخادم والتي يمكن التحقق منها بواسطة هذه الشهادة الجذرية الموثوقة ستُعتبر صالحة. - ثم نُنشئ
HttpClientباستخدام هذاSecurityContextالمخصص.
ملاحظة هامة: بهذه الطريقة، يجب أن يكون تنسيق
setTrustedCertificates()إما PEM أو PKCS12. يتطلب PKCS12 كلمة مرور للاستخدام، مما سيعرض كلمة المرور في الكود، لذلك لا يُنصح باستخدامه في الحالات الشائعة.
دعم HTTP/2: سرعة وكفاءة الجيل الجديد
HTTP/2 هو بروتوكول شبكة يوفر تحسينات كبيرة في الأداء مقارنة بـ HTTP/1.1، مثل تعدد الإرسال (Multiplexing) والضغط على العناوين (Header Compression). إذا كنت ترغب في الاستفادة من هذه الميزات مع Dio، يمكنك استخدام محول dio_http2_adapter.
[dio_http2_adapter](../plugins/http2_adapter) هو محول HttpClientAdapter لـ Dio يدعم HTTP/2. لاستخدامه، ستحتاج إلى إضافته كاعتمادية (Dependency) في ملف pubspec.yaml الخاص بمشروعك، ثم تهيئته كـ httpClientAdapter لـ Dio.
الإلغاء (Cancellation): التحكم في تدفق الطلبات
في بعض الأحيان، قد تحتاج إلى إلغاء طلب HTTP قيد التقدم، على سبيل المثال، عندما يغادر المستخدم شاشة معينة قبل اكتمال الطلب، أو عندما يصبح الطلب غير ذي صلة. يوفر Dio آلية قوية لإلغاء الطلبات باستخدام CancelToken.
يمكنك إلغاء طلب باستخدام CancelToken. يمكن مشاركة رمز واحد مع طلبات متعددة. عندما يتم استدعاء cancel() لرمز، سيتم إلغاء جميع الطلبات التي تستخدم هذا الرمز.
import 'package:dio/dio.dart';
final cancelToken = CancelToken(); // إنشاء رمز إلغاء جديد
// إجراء طلب GET وتمرير رمز الإلغاء
dio.get(url, cancelToken: cancelToken).catchError((DioException error) {
// التحقق مما إذا كان الخطأ ناتجًا عن الإلغاء
if (CancelToken.isCancel(error)) {
print('تم إلغاء الطلب: ${error.message}');
} else {
// التعامل مع الأخطاء الأخرى.
print('حدث خطأ آخر: ${error.message}');
}
});
// بعد فترة، أو بناءً على حدث معين، قم بإلغاء الطلبات المرتبطة بهذا الرمز.
token.cancel('تم الإلغاء بواسطة المستخدم'); // إلغاء الطلبات برسالة مخصصة
شرح مفصل:
final cancelToken = CancelToken();: نقوم بإنشاء مثيل جديد منCancelToken. هذا الكائن هو المفتاح للتحكم في إلغاء الطلبات.dio.get(url, cancelToken: cancelToken): عند إجراء الطلب (سواءget,post,put, إلخ)، نمررcancelTokenإلى المعلمةcancelToken..catchError((DioException error) { ... }): من المهم جداً استخدامcatchErrorللتعامل مع الاستثناءات. عندما يتم إلغاء طلب، يقوم Dio بإلقاءDioExceptionمن نوعDioExceptionType.cancel.if (CancelToken.isCancel(error)): هذه الدالة المساعدة (Helper Function) تُستخدم للتحقق بسهولة مما إذا كانDioExceptionالمستلم هو نتيجة للإلغاء. إذا كان كذلك، يمكنك التعامل مع حالة الإلغاء بشكل خاص.token.cancel('تم الإلغاء بواسطة المستخدم');: عندما ترغب في إلغاء الطلب (أو الطلبات) المرتبطة بهذا الرمز، ما عليك سوى استدعاء دالةcancel()على كائنcancelToken. يمكنك تمرير رسالة اختيارية توضح سبب الإلغاء.
هناك مثال كامل يوضح كيفية إلغاء الطلبات هنا.
توسيع فئة Dio (Extends Dio class): بناء عميلك الخاص
Dio هي فئة مجردة (Abstract Class) مع دالة بناء مصنعية (Factory Constructor)، لذلك لا نقوم بتوسيع فئة Dio مباشرة. بدلاً من ذلك، يمكننا توسيع DioForNative أو DioForBrowser، على سبيل المثال:
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
// إذا كنت في المتصفح، قم باستيراد 'package:dio/browser.dart'.
class Http extends DioForNative {
Http([BaseOptions? options]) : super(options) {
// قم ببعض التهيئة الإضافية هنا إذا لزم الأمر
// على سبيل المثال، إضافة مراقبين افتراضيين أو ضبط خيارات مخصصة.
}
}
// مثال على كيفية استخدام عميل Http المخصص:
// final myHttpClient = Http(BaseOptions(baseUrl: 'https://api.example.com'));
// final response = await myHttpClient.get('/data');
شرح مفصل:
Dioكفئة مجردة: هذا يعني أنك لا تستطيع إنشاء مثيل مباشر منDioباستخدامnew Dio(). بدلاً من ذلك، تعتمد على دالة البناء المصنعية التي تُرجع المثيل المناسب للمنصة.DioForNativeوDioForBrowser: هذه هي الفئات الملموسة (Concrete Classes) التي تُقدمها Dio للمنصات الأصلية (مثل Flutter على iOS/Android) ومنصات الويب على التوالي. عند توسيع إحداها، فإنك تحصل على جميع وظائف Dio الأساسية مع القدرة على إضافة منطقك الخاص.Http([BaseOptions? options]) : super(options): هذا هو دالة البناء لفئتك المخصصة. نقوم بتمريرoptionsإلى دالة البناء الخاصة بالفئة الأم (super(options)) لتهيئة Dio بالخيارات الأساسية.// do something: هنا يمكنك إضافة أي منطق تهيئة خاص بعميل HTTP الخاص بك، مثل إضافة مراقبين افتراضيين (Default Interceptors) أو ضبط خيارات مخصصة أخرى.
يمكننا أيضًا تنفيذ عميل Dio مخصص بالكامل باستخدام DioMixin:
import 'package:dio/dio.dart';
class MyDio with DioMixin implements Dio {
// يجب عليك هنا توفير تنفيذ لجميع الأعضاء المجردة (abstract members) في Dio.
// هذا يمنحك تحكماً كاملاً في كيفية عمل Dio، ولكنه يتطلب المزيد من العمل.
// عادةً ما يكون توسيع DioForNative أو DioForBrowser كافياً لمعظم الحالات.
}
شرح مفصل:
class MyDio with DioMixin implements Dio: هنا، نحن لا نوسعDioمباشرة، بل نستخدمDioMixinالذي يوفر تطبيقًا لمعظم وظائفDio، ثم نقوم بتنفيذ الواجهةDio. هذا يمنحك مرونة أكبر في بناء عميل Dio الخاص بك من الصفر تقريباً، ولكنه يتطلب منك توفير تنفيذ لجميع الأعضاء المجردة في الواجهةDio.
مشاركة الموارد عبر الأصول على الويب (CORS): حل تحديات المتصفح
عند العمل مع تطبيقات الويب، قد تواجه تحديات تتعلق بسياسة أمان المتصفح المعروفة باسم "مشاركة الموارد عبر الأصول" (Cross-Origin Resource Sharing - CORS). هذه السياسة تمنع صفحات الويب من إجراء طلبات HTTP إلى نطاق (Domain) مختلف عن النطاق الذي تم تحميل الصفحة منه، وذلك لأسباب أمنية.
إذا لم يكن الطلب طلبًا بسيطًا (simple request)، فسيقوم متصفح الويب بإرسال طلب CORS تمهيدي (CORS preflight request) يتحقق مما إذا كان بروتوكول CORS مفهومًا وأن الخادم على دراية باستخدام طرق وعناوين محددة.
شرح مفصل:
- الطلبات البسيطة (Simple Requests): هي طلبات HTTP التي تفي بشروط معينة (مثل استخدام طرق
GET,HEAD,POSTفقط، وعناوين معينة). هذه الطلبات لا تتطلب طلبًا تمهيديًا (Preflight Request). - الطلبات غير البسيطة (Non-Simple Requests): هي أي طلب لا يفي بشروط الطلب البسيط (مثل استخدام طرق
PUT,DELETE، أو عناوين مخصصة). قبل إرسال الطلب الفعلي، يرسل المتصفح طلبOPTIONSتمهيدي إلى الخادم للتحقق مما إذا كان الخادم يسمح بالطلب الفعلي.
يمكنك تعديل طلباتك لتتوافق مع تعريف الطلب البسيط، أو إضافة وسيط CORS (CORS middleware) لخدمتك للتعامل مع طلبات CORS.
الحلول المقترحة:
- تعديل الطلب ليكون بسيطًا: إذا أمكن، قم بتعديل طلبك ليقع ضمن فئة الطلبات البسيطة. هذا يتضمن عادةً استخدام طرق HTTP المسموح بها وعدم استخدام عناوين مخصصة.
- إضافة وسيط CORS إلى الخادم: هذا هو الحل الأكثر شيوعًا وفعالية. يتضمن ذلك تكوين الخادم الخاص بك (الواجهة الخلفية - Backend) لقبول طلبات CORS من النطاقات المحددة. عادةً ما يتم ذلك عن طريق إضافة عناوين HTTP معينة (مثل
Access-Control-Allow-Origin) إلى استجابات الخادم.