Kodax

Dio و HTTP المتقدم — Flutter المتقدم

Dio و HTTP المتقدم

حزمة http تكفي للطلبات البسيطة. Dio يضيف ما تحتاجه التطبيقات الحقيقية: إعدادات مشتركة، اعتراض الطلبات، إلغاؤها، رفع الملفات مع التقدم، ومعالجة أخطاء موحدة.

الإعداد

dependencies:
  dio: ^5.4.0

أنشئ نسخة واحدة من Dio للتطبيق (عبر حقن الاعتماديات) بإعدادات أساسية: baseUrl، مهلات الاتصال والاستقبال، والترويسات المشتركة. إنشاء Dio جديد لكل طلب يضيّع إعدادات الـ Interceptors.

Interceptors: منطق مشترك لكل الطلبات

| النوع | الاستخدام النموذجي |

|---|---|

| onRequest | إضافة رمز الدخول Authorization: Bearer ...، اللغة |

| onResponse | تسجيل، تحويل موحد |

| onError | تجديد الرمز عند 401 ثم إعادة الطلب، تحويل الأخطاء لرسائل مفهومة |

⚠️ لا تطبع الترويسات أو أجسام الطلبات في سجلات الإنتاج: قد تحتوي رموز الدخول وكلمات المرور. فعّل المسجّل في وضع التطوير فقط (kDebugMode).

أنواع الأخطاء (Dio 5)

| DioExceptionType | المعنى | رسالة للمستخدم |

|---|---|---|

| connectionTimeout / receiveTimeout | الشبكة بطيئة | "الاتصال بطيء، حاول مجدداً" |

| connectionError | لا إنترنت أو الخادم غير متاح | "تحقق من اتصالك" |

| badResponse | رد بحالة 4xx/5xx | حسب الحالة: 401 ← تسجيل دخول، 404 ← غير موجود |

| cancel | ألغينا الطلب بأنفسنا | لا شيء |

حوّل هذه الأنواع في طبقة Data إلى Failure خاصة بتطبيقك (درس المعمارية) — الواجهة لا يجب أن تعرف Dio.

الإلغاء

CancelToken يلغي طلباً لم يعد مطلوباً (المستخدم غادر الشاشة، أو كتب حرفاً جديداً في البحث). يمنع تحديث حالة شاشة أُغلقت ويوفر البيانات.

رفع الملفات مع التقدم

FormData و MultipartFile.fromFile مع onSendProgress: (sent, total) {...} لعرض شريط تقدم.

إعادة المحاولة بحكمة

أعد المحاولة للأخطاء المؤقتة فقط (مهلة، 503) وبتأخير متزايد (1ث، 2ث، 4ث). لا تعِد تلقائياً طلبات غير آمنة التكرار مثل الدفع (POST) دون مفتاح منع التكرار.

تمرين

أضف إلى المثال Interceptor يعيد المحاولة مرتين لأخطاء المهلة فقط، و CancelToken لحقل بحث يلغي الطلب السابق عند كل حرف جديد.

// ===== إعداد Dio =====
class DioClient {
  late Dio _dio;
  
  DioClient() {
    _dio = Dio(BaseOptions(
      baseUrl: 'https://api.example.com',
      connectTimeout: Duration(seconds: 10),
      receiveTimeout: Duration(seconds: 10),
      headers: {'Content-Type': 'application/json'},
    ));
    
    _setupInterceptors();
  }
  
  void _setupInterceptors() {
    // Auth Interceptor
    _dio.interceptors.add(
      InterceptorsWrapper(
        onRequest: (options, handler) {
          final token = AuthService.token;
          if (token != null) {
            options.headers['Authorization'] = 'Bearer $token';
          }
          handler.next(options);
        },
        onResponse: (response, handler) {
          handler.next(response);
        },
        onError: (error, handler) async {
          if (error.response?.statusCode == 401) {
            // Token expired - refresh
            await AuthService.refreshToken();
            // Retry request
            final opts = error.requestOptions;
            final response = await _dio.fetch(opts);
            handler.resolve(response);
          } else {
            handler.next(error);
          }
        },
      ),
    );
    
    // Logger Interceptor
    _dio.interceptors.add(PrettyDioLogger(
      requestHeader: true,
      requestBody: true,
      responseBody: true,
    ));
  }
  
  // GET Request
  Future<Response> get(String path, {Map<String, dynamic>? params}) {
    return _dio.get(path, queryParameters: params);
  }
  
  // POST Request
  Future<Response> post(String path, {dynamic data}) {
    return _dio.post(path, data: data);
  }
}

// ===== Error Handler =====
class ApiException implements Exception {
  final String message;
  final int? statusCode;
  
  ApiException({required this.message, this.statusCode});
  
  factory ApiException.fromDioError(DioException error) {
    switch (error.type) {
      case DioExceptionType.connectionTimeout:
        return ApiException(message: 'انتهت مهلة الاتصال');
      case DioExceptionType.receiveTimeout:
        return ApiException(message: 'انتهت مهلة الاستقبال');
      case DioExceptionType.badResponse:
        return _handleBadResponse(error.response);
      case DioExceptionType.connectionError:
        return ApiException(message: 'لا يوجد اتصال بالإنترنت');
      default:
        return ApiException(message: 'حدث خطأ غير متوقع');
    }
  }
  
  static ApiException _handleBadResponse(Response? response) {
    switch (response?.statusCode) {
      case 400:
        return ApiException(message: 'طلب غير صالح', statusCode: 400);
      case 401:
        return ApiException(message: 'غير مصرح', statusCode: 401);
      case 404:
        return ApiException(message: 'غير موجود', statusCode: 404);
      case 500:
        return ApiException(message: 'خطأ في الخادم', statusCode: 500);
      default:
        return ApiException(message: 'خطأ غير معروف');
    }
  }
}

// ===== Upload with Progress =====
Future<void> uploadFile(File file) async {
  final formData = FormData.fromMap({
    'file': await MultipartFile.fromFile(
      file.path,
      filename: file.path.split('/').last,
    ),
  });
  
  await _dio.post(
    '/upload',
    data: formData,
    onSendProgress: (sent, total) {
      final progress = (sent / total * 100).toStringAsFixed(0);
      print('Upload: $progress%');
    },
  );
}
اختبر فهمك