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

بناء طبقة خدمات API متكاملة

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

بناء طبقة خدمات API متكاملة

على مدار هذا الدرس تعلّمت كيفية إعداد Dio، وفكّ تسلسل النماذج المكتوبة، ومعالجة الأخطاء مركزيًا، وتخزين الاستجابات مؤقتًا، وإعادة المحاولة تلقائيًا. في هذا الدرس الأخير ستجمع كل هذه المفاهيم في فئة ApiService واحدة جاهزة للإنتاج، تكون الجسر الوحيد بين واجهة مستخدم Flutter والشبكة الخارجية. تعني طبقة الخدمات النظيفة أن الودجات لا تلمس أفعال HTTP ولا عناوين URL الأساسية ولا رموز الأخطاء مباشرةً — بل تستدعي فحسب دوال Dart معبّرة وتتلقى نماذج النطاق.

لماذا طبقة الخدمات؟ بدونها، ينزلق كود الشبكة إلى الودجات والنماذج والمستودعات، مما يجعل التطبيق هشًّا أمام تغييرات API ويجعل اختباره وحدويًا أمرًا مستحيلًا دون الاتصال بالشبكة الحقيقية. تُغلّف فئة ApiService المتماسكة كل اهتمام شبكي ويمكن حقنها أو محاكاتها في أي مكان.

1. الإعداد النهائي لعميل Dio

أنشئ نسخة Dio واحدة مع وصل الـ interceptors بالترتيب الصحيح: المصادقة أولًا (تحقن الرموز المميزة)، ثم التخزين المؤقت، ثم إعادة المحاولة، وأخيرًا السجلّ (الخارجي، كي يرى الطلب والاستجابة النهائيين).

import 'package:dio/dio.dart';
import 'package:dio_cache_interceptor/dio_cache_interceptor.dart';
import 'package:dio_smart_retry/dio_smart_retry.dart';

Dio createDio() {
  final options = BaseOptions(
    baseUrl: 'https://api.example.com/v1/',
    connectTimeout: const Duration(seconds: 10),
    receiveTimeout: const Duration(seconds: 15),
    headers: {'Accept': 'application/json'},
  );

  final dio = Dio(options);

  // 1. Auth interceptor — attach Bearer token
  dio.interceptors.add(AuthInterceptor());

  // 2. Cache interceptor — serve stale-while-revalidate
  final cacheOptions = CacheOptions(
    store: MemCacheStore(),
    policy: CachePolicy.refreshForceCache,
    maxStale: const Duration(minutes: 5),
  );
  dio.interceptors.add(DioCacheInterceptor(options: cacheOptions));

  // 3. Retry interceptor — 3 attempts on 5xx / network errors
  dio.interceptors.add(RetryInterceptor(
    dio: dio,
    retries: 3,
    retryDelays: [
      const Duration(seconds: 1),
      const Duration(seconds: 2),
      const Duration(seconds: 4),
    ],
  ));

  // 4. Logger (dev-only, stripped in release builds)
  assert(() {
    dio.interceptors.add(LogInterceptor(responseBody: true));
    return true;
  }());

  return dio;
}

2. معالجة الأخطاء مركزيًا

غلّف كل استدعاء لـ Dio في دالة مساعدة خاصة تحوّل قيم DioException إلى نوع ApiException الخاص بك. كود واجهة المستخدم لا يستورد Dio أبدًا — يلتقط فحسب ApiException.

class ApiException implements Exception {
  final String message;
  final int? statusCode;
  const ApiException(this.message, {this.statusCode});

  @override
  String toString() => 'ApiException($statusCode): $message';
}

Future<T> safeCall<T>(Future<T> Function() call) async {
  try {
    return await call();
  } on DioException catch (e) {
    switch (e.type) {
      case DioExceptionType.connectionTimeout:
      case DioExceptionType.receiveTimeout:
        throw const ApiException('Request timed out. Check your connection.');
      case DioExceptionType.badResponse:
        final code = e.response?.statusCode;
        final msg = e.response?.data?['message'] as String? ?? 'Server error';
        throw ApiException(msg, statusCode: code);
      case DioExceptionType.connectionError:
        throw const ApiException('No internet connection.');
      default:
        throw ApiException(e.message ?? 'Unexpected error');
    }
  }
}

3. دمج النماذج المكتوبة

تُعيَّن كل استجابة API فورًا إلى نموذج Dart مكتوب باستخدام مُنشئ المصنع fromJson. طبقة الخدمة تملك هذا التعيين كي يعمل باقي التطبيق مع كائنات Dart عادية، وليس قيم Map<String, dynamic> خام.

4. فئة ApiService الكاملة

اجمع كل شيء في فئة يمكن تسجيلها مرةً واحدة (عبر get_it أو Riverpod مثلًا) وحقنها أينما يلزم.

class ApiService {
  ApiService({Dio? dio}) : _dio = dio ?? createDio();

  final Dio _dio;

  // --- Users ---

  Future<User> fetchUser(int id) => safeCall(() async {
    final res = await _dio.get('users/$id');
    return User.fromJson(res.data as Map<String, dynamic>);
  });

  Future<List<User>> fetchUsers({int page = 1}) => safeCall(() async {
    final res = await _dio.get('users', queryParameters: {'page': page});
    final list = res.data['data'] as List;
    return list
        .map((e) => User.fromJson(e as Map<String, dynamic>))
        .toList();
  });

  // --- Posts ---

  Future<Post> createPost(Map<String, dynamic> body) => safeCall(() async {
    final res = await _dio.post('posts', data: body);
    return Post.fromJson(res.data as Map<String, dynamic>);
  });

  Future<void> deletePost(int id) => safeCall(() async {
    await _dio.delete('posts/$id');
  });

  // --- Auth ---

  Future<String> login(String email, String password) => safeCall(() async {
    final res = await _dio.post('auth/login', data: {
      'email': email,
      'password': password,
    });
    return res.data['token'] as String;
  });
}
نصيحة: اجعل مُنشئ ApiService قابلًا للحقن. اقبل مَعاملًا اختياريًا Dio? — في الإنتاج يأخذ القيمة الافتراضية createDio()، أما في الاختبارات فتمرّر MockDio أو Dio مُوجَّه نحو خادم محاكاة محلي دون تعديل سطر واحد من كود الإنتاج.

5. استهلاك الخدمة من ودجت

تبقى الودجات رفيعة: تستدعي الخدمة، وتعالج ApiException، وتعرض النتيجة. لا تتسرب أي تفاصيل HTTP إلى طبقة واجهة المستخدم.

class UserScreen extends StatefulWidget {
  final int userId;
  const UserScreen({super.key, required this.userId});

  @override
  State<UserScreen> createState() => _UserScreenState();
}

class _UserScreenState extends State<UserScreen> {
  late final ApiService _api;
  User? _user;
  String? _error;
  bool _loading = true;

  @override
  void initState() {
    super.initState();
    _api = ApiService(); // استخدم Provider أو get_it في التطبيقات الحقيقية
    _load();
  }

  Future<void> _load() async {
    try {
      final user = await _api.fetchUser(widget.userId);
      if (mounted) setState(() { _user = user; _loading = false; });
    } on ApiException catch (e) {
      if (mounted) setState(() { _error = e.message; _loading = false; });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_loading) return const CircularProgressIndicator();
    if (_error != null) return Text('Error: $_error');
    return Text(_user!.name);
  }
}
تحذير: لا تستدعِ setState() بعد إلغاء تحميل الودجت. احرص دائمًا على حراسة الدوال غير المتزامنة بـ if (mounted) كما هو مبيّن أعلاه، لتفادي خطأ "setState called after dispose" في زمن التشغيل.

6. ملخص مبادئ البنية

تتبع طبقة الشبكة المنظّمة جيدًا هذه المبادئ:

  • نسخة Dio واحدة — تُنشأ مرةً واحدة وتُشارك في كل مكان عبر حقن التبعيات
  • Interceptors بالترتيب — مصادقة، ثم تخزين مؤقت، ثم إعادة محاولة، ثم سجلّ
  • نماذج مكتوبة فقط — لا تهرب الخرائط الخام من طبقة الخدمة أبدًا
  • نوع خطأ واحد — يحلّ ApiException محل جميع استثناءات Dio للمستدعين
  • قابل للاختبار بالتصميم — مَعامل Dio القابل للحقن يتيح اختبارات وحدوية سريعة ومعزولة
  • طبقة واجهة مستخدم رفيعة — الودجات تستدعي دوال النطاق لا أفعال HTTP
الفكرة الرئيسية: نمط ApiService هو تتويج لكل ما تعلّمته في هذا الدرس. يعزل كل اهتمام شبكي — الإعداد، والتسلسل، والتخزين المؤقت، وإعادة المحاولة، وتوحيد الأخطاء — خلف واجهة برمجية Dart نظيفة. يصبح كود واجهة المستخدم أسهل قراءةً واختبارًا وصيانةً، ويمكن لمنطق الشبكة أن يتطور بصورة مستقلة عن بقية التطبيق.