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