معترضات Dio: التسجيل ورموز المصادقة وإعادة المحاولة
معترضات Dio: التسجيل ورموز المصادقة وإعادة المحاولة
مع نمو تطبيق Flutter الخاص بك، ستجد نفسك تكرر نفس الكود النمطي في كل استدعاء للشبكة: إرفاق ترويسة Authorization، وطباعة تفاصيل الطلب إلى وحدة التحكم، وإعادة محاولة طلب ما حين يكون الخادم غير متاح مؤقتاً. معترضات Dio تحل هذه المشاكل الثلاث في نقطة واحدة دون المساس بمواقع الاستدعاء الفردية. المعترض هو طبقة وسيطة تجلس بين كود تطبيقك ومحرك HTTP الخاص بـ Dio — يمكنه فحص كل طلب واستجابة وخطأ يمر عبر العميل أو تعديله أو إيقافه.
كيف تعمل خط أنابيب المعترضات
يعالج Dio كل تفاعل HTTP عبر ثلاث نقاط ربط:
- onRequest — يُستدعى قبيل إرسال الطلب إلى الخادم. مثالي لحقن الترويسات أو تسجيل البيانات الصادرة.
- onResponse — يُستدعى فور استلام استجابة ناجحة. استخدمه لتسجيل أجسام الاستجابة أو توحيد أشكال البيانات.
- onError — يُستدعى عند فشل الطلب (خطأ في الشبكة، أو حالة غير 2xx، أو انتهاء مهلة). استخدمه للإعادة المحاولة أو تحديث الرموز أو تحويل الأخطاء إلى استثناءات الحقل.
تتلقى كل نقطة ربط كائن handler. يجب عليك استدعاء واحد بالضبط من handler.next() أو handler.resolve() أو handler.reject() لتمرير التحكم إلى المعترض التالي أو إعادته إلى المستدعي.
بناء معترض التسجيل
يمنحك معترض التسجيل رؤية على كل تفاعل شبكة أثناء التطوير. يطبع الطريقة وعنوان URL وترويسات الطلب ورمز الحالة ومعاينة مبتورة لجسم الاستجابة دون أي تغييرات على كود موقع الاستدعاء.
LoggingInterceptor — التنفيذ الكامل
import 'package:dio/dio.dart';
class LoggingInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
print('--> ${options.method} ${options.uri}');
options.headers.forEach((k, v) => print(' Header: $k: $v'));
if (options.data != null) print(' Body: ${options.data}');
handler.next(options); // مرر الطلب إلى المعترض التالي / محرك Dio
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
final body = response.data.toString();
final preview = body.length > 200 ? '${body.substring(0, 200)}...' : body;
print('<-- ${response.statusCode} ${response.requestOptions.uri}');
print(' Body preview: $preview');
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
print('ERR ${err.response?.statusCode} ${err.requestOptions.uri}: ${err.message}');
handler.next(err); // انشر الخطأ دون تغيير
}
}
بناء معترض رمز المصادقة
بدلاً من إضافة 'Authorization': 'Bearer \$token' يدوياً لكل استدعاء http.get()، يقرأ معترض المصادقة الرمز من خدمة تخزين ويرفقه تلقائياً. يمكن أن يكون مصدر الرمز متغيراً في الذاكرة أو SharedPreferences أو حزمة تخزين آمنة — المعترض لا يهتم.
AuthInterceptor — حقن رمز Bearer في كل طلب
import 'package:dio/dio.dart';
/// يقرأ رمز الوصول الحالي ويرفقه كترويسة Bearer.
/// استبدل [_tokenStore] بمصدر الرمز الخاص بك (SharedPreferences، إلخ).
class AuthInterceptor extends Interceptor {
final TokenStore _tokenStore;
AuthInterceptor(this._tokenStore);
@override
void onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
final token = await _tokenStore.getAccessToken();
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// إذا أعاد الخادم 401، ربما انتهت صلاحية الرمز.
// حاول تحديث الرمز بصمت قبل إعادة محاولة الطلب الأصلي.
if (err.response?.statusCode == 401) {
try {
final newToken = await _tokenStore.refreshAccessToken();
// استنسخ الطلب الأصلي مع الرمز الجديد وأعد محاولته.
final opts = err.requestOptions
..headers['Authorization'] = 'Bearer $newToken';
final dio = Dio(); // استخدم عميلاً جديداً لتجنب حلقات المعترضات
final response = await dio.fetch(opts);
return handler.resolve(response); // اختصر الخطأ
} catch (_) {
// فشل التحديث أيضاً — انشر خطأ 401 الأصلي.
}
}
handler.next(err);
}
}
/// الواجهة الدنيا التي يجب أن يستوفيها تخزين الرمز لديك.
abstract class TokenStore {
Future<String?> getAccessToken();
Future<String> refreshAccessToken();
}
بناء معترض إعادة المحاولة
الأعطال العابرة — اضطرابات DNS أو إعادة تشغيل الخادم المؤقتة أو اتصالات الجوال المتذبذبة — يجب ألا تظهر كأخطاء للمستخدم. يعيد معترض إعادة المحاولة إرسال الطلب بشفافية حتى N مرات مع تأخير تصاعدي أسي.
RetryInterceptor — التراجع الأسي عند أخطاء الشبكة
import 'dart:async';
import 'package:dio/dio.dart';
class RetryInterceptor extends Interceptor {
final Dio dio;
final int maxRetries;
final Duration initialDelay;
RetryInterceptor({
required this.dio,
this.maxRetries = 3,
this.initialDelay = const Duration(seconds: 1),
});
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
// أعد المحاولة فقط عند أخطاء الاتصال / انتهاء المهلة، وليس عند ردود 4xx.
final isNetworkError =
err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.receiveTimeout ||
err.type == DioExceptionType.connectionError;
final attempt = (err.requestOptions.extra['retryCount'] as int?) ?? 0;
if (isNetworkError && attempt < maxRetries) {
// خزّن عدد المحاولات في خريطة extra للطلب.
err.requestOptions.extra['retryCount'] = attempt + 1;
// التراجع الأسي: 1 ث، 2 ث، 4 ث، ...
final delay = initialDelay * (1 << attempt);
await Future.delayed(delay);
try {
final response = await dio.fetch(err.requestOptions);
return handler.resolve(response);
} on DioException catch (retryErr) {
return handler.reject(retryErr);
}
}
handler.next(err); // استُنفدت المحاولات أو خطأ غير قابل للإعادة
}
}
ربط المعترضات بنسخة Dio
سجّل جميع المعترضات مرة واحدة عند إنشاء عميل Dio — عادةً في مُحدد خدمة أو إعداد حقن تبعيات. الترتيب مهم: المصادقة أولاً (تضيف الترويسة)، وإعادة المحاولة ثانياً (تمسك الأخطاء)، والتسجيل أخيراً (يرى الطلب النهائي).
مصنع DioClient — تركيب المعترضات الثلاثة
import 'package:dio/dio.dart';
Dio createDioClient(TokenStore tokenStore) {
final dio = Dio(
BaseOptions(
baseUrl: 'https://api.example.com',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 15),
headers: {'Accept': 'application/json'},
),
);
dio.interceptors.addAll([
AuthInterceptor(tokenStore), // 1. إرفاق ترويسة المصادقة
RetryInterceptor(dio: dio), // 2. إعادة محاولة الأعطال العابرة
LoggingInterceptor(), // 3. تسجيل الطلب والاستجابة النهائيين
]);
return dio;
}
RetryInterceptor محاولة طلب، فإنه يستدعي داخلياً dio.fetch() على نفس نسخة Dio، مما يُعيد تشغيل جميع المعترضات — بما فيها AuthInterceptor. هذا بالضبط ما تريده: سيحصل الطلب المُعاد على ترويسة مصادقة جديدة. فقط تأكد من فحص عداد المحاولات المخزّن في extra لتجنب حلقة لا نهائية.Dio ثانية داخل AuthInterceptor.onError مرفقاً بها نفس AuthInterceptor. هذا يُنشئ حلقة معترضات: يطلق طلب التحديث خطأ 401 آخر، الذي يطلق تحديثاً آخر، وهكذا إلى ما لا نهاية. استخدم Dio() منفصلاً وعادياً بدون معترضات مصادقة لاستدعاء تحديث الرمز.ملخص
تمنحك معترضات Dio مكاناً نظيفاً ومركزياً للتعامل مع الاهتمامات المشتركة. يطبع معترض التسجيل كل طلب واستجابة دون تلويث كود موقع الاستدعاء. يرفق معترض المصادقة رموز Bearer تلقائياً ويمكنه تحديث رمز منتهي الصلاحية بصمت عند خطأ 401. يُعيد معترض إعادة المحاولة إرسال الأعطال العابرة مع تراجع أسي حتى لا يرى المستخدمون أبداً "خطأ في الاتصال" لانقطاع لثانية واحدة. سجّل المعترضات بالترتيب: مصادقة ← إعادة محاولة ← تسجيل، حتى ترى كل طبقة ما ستتصرف عليه الطبقات التالية.