Flutter: الشبكات وREST API
تطبيق بدون شبكة هو جزيرة — طلبات API هي الجسر نحو البر الرئيسي.
📋 المتطلبات السابقة: يجب أن تتقن ما يلي أولًا
- الدرس 10: القوائم والتمرير
1. ما ستتعلمه
- حزمة
httpطلبات GET/POST/PUT/DELETE الأساسية Dioبعمق: المعترضات، CancelToken، رفع FormData، إعادة المحاولة عند انتهاء المهلة- تسلسل JSON: توليد أكواد
json_serializable+build_runnerمقابل التحليل اليدوي - استراتيجية معالجة الأخطاء: اعتراض موحد لاستثناءات الشبكة / مصادقة 401 / أخطاء الخادم 500
- طبقة مستودع Dio لـ ShopApp، التكامل مع نقطة نهاية
/api/v1/products
2. قصة حقيقية عن فشل طلبات الشبكة
(1) المشكلة: أخطاء الشبكة تُعطل التطبيق
في يوم إطلاق ShopApp، حصل المستخدمون على اتصالات غير مستقرة على شاشات بيضاء متعطلة — لا try-catch، لا مهلة زمنية، لا إعادة محاولة. الأسوأ، عندما انتهت صلاحية Token، أرجعت جميع الطلبات 401 ولم يكن للتطبيق آلية تحديث تلقائية، مما أجبر المستخدمين على تسجيل الدخول مجددًا. ألفا تقييم سلبي في يوم واحد.
(2) حل معترضات Dio
آلية معترضات Dio تُرفق الرموز بشكل موحد قبل خروج الطلبات، وتتعامل تلقائيًا مع استجابات 401 بتحديث الرموز، وتُعيد المحاولة عند انتهاء مهلة الشبكة.
import 'package:dio/dio.dart';
// ⚙️ تثبيت التبعية: flutter pub add dio
// Assume token and refreshToken are defined elsewhere
// String token = '...';
// Future<String> refreshToken() async { ... }
// Auth interceptor: auto-attach token + auto-refresh on 401
dio.interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
},
onError: (error, handler) async {
if (error.response?.statusCode == 401) {
token = await refreshToken();
return handler.resolve(await dio.fetch(error.requestOptions));
}
handler.next(error);
},
));
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
(3) الفائدة: صفر أعطال متعلقة بالشبكة
بعد استخدام معترضات Dio، يحصل بوب على تحديث تلقائي لرمز 401، وإعادة محاولة 3 مرات عند انتهاء المهلة، وعرض موحد للأخطاء. ينخفض معدل الأعطال المتعلقة بالشبكة من 5% إلى 0.01%.
3. طلبات HTTP الأساسية
(1) عمليات CRUD بحزمة http
| الطريقة | فعل HTTP | الغرض |
|---|---|---|
http.get |
GET | استرجاع الموارد |
http.post |
POST | إنشاء موارد |
http.put |
PUT | تحديث موارد |
http.delete |
DELETE | حذف موارد |
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: طلبات أساسية بحزمة http
import 'package:http/http.dart' as http;
import 'dart:convert';
// ⚙️ تثبيت التبعية: flutter pub add http
// Custom class definition sources:
// - Product: see Lesson 5 json_serializable model in this lesson
// - ApiException: see Lesson 6 unified error handling in this lesson
// Assume token is defined elsewhere
// String token = '...';
class ProductApi {
static const String baseUrl = 'https://api.shopapp.com/v1';
Future<List<Product>> getProducts({int page = 1}) async {
final response = await http.get(
Uri.parse('$baseUrl/products?page=$page'),
headers: {'Authorization': 'Bearer $token'},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
return (data['items'] as List).map((j) => Product.fromJson(j)).toList();
}
throw ApiException(response.statusCode, response.body);
}
Future<Product> createProduct(Product product) async {
final response = await http.post(
Uri.parse('$baseUrl/products'),
headers: {'Content-Type': 'application/json', 'Authorization': 'Bearer $token'},
body: jsonEncode(product.toJson()),
);
if (response.statusCode == 201) {
return Product.fromJson(jsonDecode(response.body));
}
throw ApiException(response.statusCode, response.body);
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
4. Dio بعمق
(1) مقارنة http مقابل Dio
| الميزة | http | Dio |
|---|---|---|
| CRUD الأساسي | ✅ | ✅ |
| المعترضات | ❌ | ✅ |
| CancelToken | ❌ | ✅ |
| رفع FormData | يدوي | ✅ |
| إعادة محاولة المهلة | يدوي | ✅ |
| تسجيل الطلبات | يدوي | ✅ (dio_logger) |
| إعدادات عامة | ❌ | ✅ BaseOptions |
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: إعدادات Dio الأساسية
import 'package:dio/dio.dart';
// ⚙️ تثبيت التبعية: flutter pub add dio
// Custom class definition sources:
// - Product: see Lesson 5 json_serializable model in this lesson
// - Order/Cart: see Lesson 14 Phase 2 practice
final dio = Dio(BaseOptions(
baseUrl: 'https://api.shopapp.com/v1',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
));
// GET request
Future<List<Product>> getProducts({int page = 1}) async {
final response = await dio.get('/products', queryParameters: {'page': page});
return (response.data['items'] as List).map((j) => Product.fromJson(j)).toList();
}
// POST request
Future<Order> createOrder(Cart cart) async {
final response = await dio.post('/orders', data: cart.toJson());
return Order.fromJson(response.data);
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: معترض المصادقة + تحديث الرمز
import 'package:dio/dio.dart';
// Custom class definition sources:
// - SecureStorage: see Lesson 13 Flutter Secure Storage
// - AuthService: see Lesson 14 Auth Notifier
class AuthInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
final token = SecureStorage.getToken();
if (token != null) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) async {
if (err.response?.statusCode == 401) {
try {
final newToken = await _refreshToken();
SecureStorage.saveToken(newToken);
// Retry original request with new token
err.requestOptions.headers['Authorization'] = 'Bearer $newToken';
final response = await dio.fetch(err.requestOptions);
return handler.resolve(response);
} catch (_) {
// Refresh failed, redirect to login
AuthService.logout();
}
}
handler.next(err);
}
Future<String> _refreshToken() async {
final refreshToken = SecureStorage.getRefreshToken();
final response = await Dio().post(
'https://api.shopapp.com/v1/auth/refresh',
data: {'refresh_token': refreshToken},
);
return response.data['access_token'];
}
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: إلغاء طلب CancelToken
import 'package:dio/dio.dart';
// ⚙️ تثبيت التبعية: flutter pub add dio
CancelToken? _cancelToken;
void searchProducts(String query) {
_cancelToken?.cancel('New search request');
_cancelToken = CancelToken();
dio.get('/products/search', queryParameters: {'q': query},
cancelToken: _cancelToken);
}
// Cancel on dispose
@override
void dispose() {
_cancelToken?.cancel();
super.dispose();
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
5. تسلسل JSON
(1) مقارنة طرق التسلسل
| الطريقة | المزايا | العيوب | حالة الاستخدام |
|---|---|---|---|
| fromJson/toJson يدوي | بدون تبعيات، تحكم كامل | متكرر، عُرضة للأخطاء | نماذج بسيطة قليلة |
| json_serializable | آمن الأنواع، توليد تلقائي | يحتاج build_runner | نماذج كثيرة |
| freezed | ثابت + copyWith + اتحاد | تبعيات أكثر | نماذج مجال معقدة |
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: نموذج json_serializable
import 'package:json_annotation/json_annotation.dart';
// ⚙️ تثبيت التبعية: flutter pub add json_annotation
// ⚙️ تبعية تطوير: flutter pub add --dev json_serializable build_runner
part 'product.g.dart';
@JsonSerializable()
class Product {
final int id;
final String name;
@JsonKey(name: 'unit_price')
final double price;
@JsonKey(name: 'image_url')
final String? imageUrl;
final double rating;
const Product({
required this.id,
required this.name,
required this.price,
this.imageUrl,
this.rating = 0.0,
});
factory Product.fromJson(Map<String, dynamic> json) => _$ProductFromJson(json);
Map<String, dynamic> toJson() => _$ProductToJson(this);
}
// Run: dart run build_runner build --delete-conflicting-outputs
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
6. معالجة الأخطاء الموحدة
▶ مثال
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
: ApiException + معترض الأخطاء
import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
// ⚙️ تثبيت التبعية: flutter pub add dio
class ApiException implements Exception {
final int? statusCode;
final String message;
final String? errorCode;
ApiException(this.statusCode, this.message, {this.errorCode});
factory ApiException.fromDioError(DioException error) {
switch (error.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return ApiException(null, 'Connection timeout. Please try again.');
case DioExceptionType.connectionError:
return ApiException(null, 'No internet connection.');
case DioExceptionType.badResponse:
final status = error.response?.statusCode;
final msg = error.response?.data['message'] ?? 'Server error';
if (status == 401) return ApiException(401, 'Session expired. Please login again.');
if (status == 403) return ApiException(403, 'Access denied.');
if (status == 404) return ApiException(404, 'Resource not found.');
if (status == 422) return ApiException(422, msg, errorCode: error.response?.data['code']);
if (status != null && status >= 500) return ApiException(status, 'Server error. Please try later.');
return ApiException(status, msg);
default:
return ApiException(null, 'Unexpected error.');
}
}
}
// Usage in UI
try {
final products = await productRepo.getProducts();
} on ApiException catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(e.message)),
);
}
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
7. مثال كامل: مستودع Dio لـ ShopApp
import 'package:dio/dio.dart';
// ⚙️ تثبيت التبعية: flutter pub add dio
// Custom class definition sources:
// - Product: see Lesson 5 json_serializable model in this lesson
// - CartItem/Address/Order: see Lesson 14 Phase 2 practice
// - AuthInterceptor: see Lesson 4 Auth interceptor in this lesson
class ProductRepository {
final Dio _dio;
ProductRepository(this._dio);
Future<List<Product>> getProducts({
int page = 1,
int limit = 20,
String? category,
String? search,
}) async {
final params = {'page': page, 'limit': limit};
if (category != null) params['category'] = category;
if (search != null) params['q'] = search;
final response = await _dio.get('/products', queryParameters: params);
return (response.data['items'] as List)
.map((j) => Product.fromJson(j))
.toList();
}
Future<Product> getProductById(int id) async {
final response = await _dio.get('/products/$id');
return Product.fromJson(response.data);
}
Future<Order> createOrder(List<CartItem> items, Address address) async {
final response = await _dio.post('/orders', data: {
'items': items.map((i) => {'product_id': i.product.id, 'quantity': i.quantity}).toList(),
'shipping_address': address.toJson(),
});
return Order.fromJson(response.data);
}
Future<String> uploadProductImage(String filePath) async {
final formData = FormData.fromMap({
'image': await MultipartFile.fromFile(filePath),
});
final response = await _dio.post('/upload', data: formData);
return response.data['url'];
}
}
// Dio singleton setup
final dio = Dio(BaseOptions(
baseUrl: 'https://api.shopapp.com/v1',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 30),
))
..interceptors.add(AuthInterceptor())
..interceptors.add(LogInterceptor(requestBody: true, responseBody: true));
final productRepo = ProductRepository(dio);
> الإخراج: شغّل محليًا باستخدام Flutter SDK (Flutter 3.x / Dart 3.x). خادم Piston لا يحتوي على Flutter؛ استخدم `flutter run` على جهازك المحلي للمقارنة. قد تختلف واجهة/حالة المستخدم الفعلية قليلاً بين المنصات.
❓ أسئلة شائعة
dart run build_runner watch --delete-conflicting-outputs لمراقبة تغييرات الملفات والتوليد التلقائي المستمر.HttpClientAdapter لتخطي التحقق؛ في الإنتاج، يجب التحقق من الشهادات.📖 ملخص
- حزمة http مناسبة للسيناريوهات البسيطة؛ Dio للمشاريع الإنتاجية التي تحتاج معترضات وإلغاء وإعادة محاولة
- المعترضات تتعامل بشكل موحد مع إرفاق الرموز وتحديث 401 التلقائي
- CancelToken يُلغي الطلبات القديمة (مثل debounce لمربع البحث)
- توليد أكواد json_serializable آمن الأنواع ويقلل الأخطاء اليدوية
- ApiException يُغلف الأخطاء بشكل موحد؛ طبقة UI تحتاج فقط لالتقاط نوع استثناء واحد
📝 تمارين
- أساسي (⭐): استخدم حزمة http لتنفيذ طلب GET، جلب قائمة منتجات، وطباعة JSON.
- متوسط (⭐⭐): غلّف مستودعًا بـ Dio، أضف معترض مصادقة لإرفاق Bearer Token تلقائيًا، وتحديث تلقائي عند 401.
- متقدم (⭐⭐⭐): نفّذ طبقة شبكات كاملة: Dio + معترض مصادقة + معترض سجل + CancelToken لـ debounce البحث + ApiException لمعالجة أخطاء موحدة + نماذج json_serializable.