الآن ندخل إلى واحدة من أهم المراحل في فهم الـ API و Dio.
معظم المبتدئيين يتعلمون:
final response = await dio.get(...);
print(response.data);Dartثم ينتقلون مباشرة إلى Models دون أن يفهموا:
- ما هو Response؟
- ما هو data؟
- لماذا أحياناً تكون List؟
- لماذا أحياناً تكون Map؟
- لماذا لا نستخدم jsonDecode مع Dio؟
- ما هو dynamic؟
- كيف يحول Dio الـ JSON تلقائياً؟
وهنا يبدأ التخبط لاحقاً.
المرحلة الثانية: تشريح Response بالكامل
لنبدأ من هذا الكود:
Response response = await dio.get(
'https://jsonplaceholder.typicode.com/posts',
);Dartما الذي يحدث خلف الكواليس؟
عندما يصل الطلب إلى السيرفر يحدث التالي:
السيرفر يعيد:
HTTP/1.1 200 OK
Content-Type: application/jsonDartثم يعيد Body:
[
{
"userId": 1,
"id": 1,
"title": "test",
"body": "hello"
}
]DartDio يستقبل هذا الرد ويضعه داخل كائن اسمه:
ResponseDartشكل Response الحقيقي
بشكل مبسط:
class Response<T> {
T? data;
int? statusCode;
String? statusMessage;
Headers headers;
RequestOptions requestOptions;
}Dartهذا ليس الكود الحقيقي بالكامل لكنه يوضح الفكرة.
ماذا يعني Response<T> ؟
لاحظ:
Response<T>Dartوليس:
ResponseDartهنا يوجد Generic.
ما هو Generic ؟
مثال:
List<String>Dartمعناها:
هذه القائمة تحتوي Strings فقطDartمثال:
List<int>Dartمعناها:
هذه القائمة تحتوي أرقام فقطDartنفس الفكرة هنا:
Response<T>Dartأي:
نوع البيانات داخل response.dataDartلماذا غالباً نكتب
Response responseDartبدلاً من:
Response<List>Dartلأن Dio يحدد النوع تلقائياً غالباً.
لنطبع Response كاملاً
print(response);Dartستشاهد شيئاً قريباً من:
Instance of ResponseDartلذلك نحتاج الوصول للخصائص.
أول خاصية
response.dataDartوهذه أهم خاصية في Dio بالكامل.
ما هو data ؟
هو Body الخاص بالرد.
تذكر:
السيرفر يعيد:
Headers
BodyDartDio يخزن Body هنا:
response.dataDartمثال عملي
السيرفر يعيد:
{
"id":1,
"name":"Ahmed"
}Dartعندها:
print(response.data);Dartالنتيجة:
{
id:1,
name:Ahmed
}Dartمثال آخر
إذا أعاد السيرفر:
[
{
"id":1
},
{
"id":2
}
]Dartعندها:
response.dataDartتصبح:
[
{id:1},
{id:2}
]Dartسؤال مهم جداً
كيف عرف Dio ذلك؟
كيف عرف أن هذه List؟
أو Map؟
السر هنا
السيرفر يرسل:
Content-Type: application/jsonDartفيرى Dio أن الرد JSON.
فيقوم تلقائياً بـ:
jsonDecode(...)Dartخلف الكواليس.
لذلك لا نفعل هذا
jsonDecode(response.data);Dartخطأ.
لماذا خطأ؟
لأن Dio قام بالتحويل مسبقاً.
مقارنة مع package:http
في package:http كنا نفعل:
var response = await http.get(...);
jsonDecode(response.body);Dartأما Dio:
response.dataDartجاهزة مباشرة.
ما نوع response.data ؟
جرّب:
print(response.data.runtimeType);Dartعند جلب posts
https://jsonplaceholder.typicode.com/postsDartالنتيجة:
List<dynamic>Dartلماذا List ؟
لأن JSON تبدأ بـ:
[
]Dartأي Array.
قاعدة ذهبية
إذا بدأ JSON بـ:
[
]Dartفالنوع:
ListDartوإذا بدأ بـ
{
}Dartفالنوع:
MapDartمثال
[
{
"id":1
}
]Dart↓
List<dynamic>Dartمثال
{
"id":1
}Dart↓
Map<String,dynamic>Dartما معنى dynamic ؟
من أكثر الكلمات التي تخيف المبتدئين.
مثال
dynamic value;Dartمعناها:
قد يكون أي شيءDartيمكن أن يكون
StringDartأو:
intDartأو:
ListDartأو:
MapDartلذلك
response.dataDartنوعها غالباً:
dynamicDartلأن Dio لا يعرف مسبقاً ماذا سيرجع السيرفر.
كيف نكتشف النوع؟
مثال:
print(response.data.runtimeType);DartruntimeType
تعني:
ما نوع الكائن أثناء التشغيل؟Dartمثال
print(response.data.runtimeType);Dartقد تطبع:
List<dynamic>Dartأو:
_Map<String,dynamic>Dartالحالة الأولى: List
مثال:
[
{
"id":1
},
{
"id":2
}
]Dartالوصول لأول عنصر
print(response.data[0]);Dartالوصول للـ id
print(response.data[0]['id']);Dartالناتج:
1Dartالحالة الثانية: Map
مثال:
{
"id":1,
"name":"Ahmed"
}Dartالوصول للقيم
print(response.data['id']);Dartماذا لو أخطأت؟
مثال:
الرد الحقيقي:
MapDartوأنت كتبت:
response.data[0]Dartستحصل على Exception.
لأن:
Map ≠ ListDartخاصية statusCode
print(response.statusCode);Dartماذا تعني؟
هي حالة الطلب.
مثال
200Dartنجاح.
مثال
404Dartالرابط غير موجود.
مثال
500Dartخطأ داخل السيرفر.
خاصية statusMessage
print(response.statusMessage);Dartمثال:
OKDartأو:
Not FoundDartخاصية headers
print(response.headers);Dartقد ترى:
content-type: application/jsonDartلماذا مهمة؟
لأنها تخبرك:
- نوع البيانات
- التشفير
- الكوكيز
- معلومات إضافية
خاصية requestOptions
هذه من الخصائص التي لا يشرحها الكثيرون.
print(response.requestOptions.path);Dartماذا تحتوي؟
الطلب الأصلي الذي أرسلته.
مثلاً:
response.requestOptions.methodDartالناتج:
GETDartresponse.requestOptions.pathDartالناتج:
/postsDartresponse.requestOptions.headersDartالناتج:
كل Headers التي أرسلتهاDartتجربة عملية مهمة جداً
ضع هذا الكود:
Future<void> getPosts() async {
Response response = await dio.get(
'https://jsonplaceholder.typicode.com/posts',
);
print('DATA');
print(response.data);
print('TYPE');
print(response.data.runtimeType);
print('STATUS');
print(response.statusCode);
print('MESSAGE');
print(response.statusMessage);
print('HEADERS');
print(response.headers);
}Dartوشاهد النتائج بنفسك.
ماذا يجب أن يفهم الطالب قبل الانتقال للـ Models؟
يجب أن يكون قادراً على الإجابة عن هذه الأسئلة:
✅ ما هو Response؟
✅ ما الفرق بين Body و Headers؟
✅ أين توجد البيانات الحقيقية؟
✅ ما هو response.data؟
✅ لماذا Dio لا يحتاج jsonDecode؟
✅ متى تكون data List؟
✅ متى تكون data Map؟
✅ ما هو dynamic؟
✅ ما هو runtimeType؟
✅ ما هو statusCode؟
✅ ما هو requestOptions؟
إذا فهم هذه النقاط فعلاً، فمرحلة Model + fromJson + toJson ستصبح سهلة جداً، لأن الطالب أصبح يعرف بالضبط ما الذي يستلمه من السيرفر وكيف يبدو داخل Dart.
المرحلة التالية
الآن نصل إلى المرحلة التي تعتبر نقطة التحول الحقيقية في تعلم الـ API.
قبل هذه المرحلة كان الطالب يتعامل مع البيانات هكذا:
response.data[0]['title']Dartأو:
response.data['name']Dartوهذا مناسب للتجارب السريعة فقط.
لكن في التطبيقات الحقيقية لا نعمل بهذه الطريقة.
المرحلة الثالثة: فهم JSON وتحويله إلى Model
لماذا نحتاج Model أصلاً؟
لنفترض أن السيرفر أعاد:
{
"userId": 1,
"id": 1,
"title": "Flutter",
"body": "Learning Dio"
}Dartالطريقة البدائية:
print(response.data['title']);Dartالمشكلة:
إذا أخطأت بالحرف:
print(response.data['titl']);Dartلن يكتشف Dart الخطأ أثناء الكتابة.
سيظهر Runtime Error لاحقاً.
الحل
ننشئ كلاس يمثل هذا الـ JSON.
class PostModel {
}Dartهذا الكلاس يصبح نسخة Dart من البيانات القادمة من السيرفر.
ما هو Model؟
الـ Model هو تمثيل للبيانات داخل التطبيق.
مثال:
JSON:
{
"id": 1,
"name": "Ahmed"
}DartModel:
class UserModel {
final int id;
final String name;
UserModel({
required this.id,
required this.name,
});
}Dartلماذا هذا أفضل؟
بدلاً من:
response.data['name']Dartنكتب:
user.nameDartوهذا:
✅ أوضح
✅ أسهل
✅ أقل أخطاء
✅ يدعم Auto Complete
إنشاء أول Model
أنشئ ملف:
models/post_model.dartDartتحليل JSON أولاً
الـ API تعيد:
{
"userId": 1,
"id": 1,
"title": "sunt aut facere",
"body": "quia et suscipit..."
}Dartإذن لدينا 4 حقول.
إنشاء الخصائص
class PostModel {
final int userId;
final int id;
final String title;
final String body;
}Dartلماذا final؟
لأن البيانات القادمة من السيرفر غالباً لا نريد تغييرها مباشرة.
مثال:
post.title = 'new title';Dartغير مسموح.
وهذا يجعل البيانات أكثر أماناً.
إنشاء Constructor
class PostModel {
final int userId;
final int id;
final String title;
final String body;
PostModel({
required this.userId,
required this.id,
required this.title,
required this.body,
});
}Dartما معنى required؟
تعني:
لا يمكن إنشاء الكائن بدون هذه القيمةDartمثال:
PostModel(
userId: 1,
id: 1,
title: 'Flutter',
body: 'Dio',
);Dartصحيح.
أما:
PostModel(
id: 1,
);Dartخطأ.
المشكلة الحالية
لدينا JSON:
{
"userId": 1,
"id": 1,
"title": "Flutter",
"body": "Dio"
}Dartلكن Constructor يحتاج:
PostModel(...)Dartكيف نحول JSON إلى Object؟
هنا يأتي fromJson
ما هو fromJson؟
هو Factory Constructor مسؤول عن تحويل:
JSONDartإلى:
PostModelDartالشكل الأساسي
factory PostModel.fromJson(
Map<String, dynamic> json,
) {
}Dartشرح كلمة factory
قبل الفهم التقني:
الـ Constructor العادي:
PostModel(...)Dartينشئ كائناً جديداً دائماً.
أما factory:
يمكنه إنشاء الكائن بطريقة خاصة.
في حالتنا:
سيقرأ JSON ثم ينشئ Model.
لماذا Map<String,dynamic> ؟
لأن JSON بعد أن يحوله Dio يصبح:
{
'userId': 1,
'id': 1,
'title': 'Flutter',
'body': 'Dio'
}Dartوهذا في Dart عبارة عن:
Map<String,dynamic>Dartبناء fromJson
factory PostModel.fromJson(
Map<String, dynamic> json,
) {
return PostModel(
userId: json['userId'],
id: json['id'],
title: json['title'],
body: json['body'],
);
}Dartماذا يحدث هنا؟
عندما نستدعي:
PostModel.fromJson(json)Dartيقوم Dart بتنفيذ:
return PostModel(...)Dartثم يعيد الكائن.
تجربة عملية
لدينا:
Map<String,dynamic> json = {
'userId':1,
'id':1,
'title':'Flutter',
'body':'Learning Dio'
};Dartالتحويل
PostModel post =
PostModel.fromJson(json);Dartالنتيجة
الآن:
print(post.title);Dartتعطي:
FlutterDartبدلاً من:
response.data['title']Dartكيف نحول List كاملة؟
السيرفر يعيد:
[
{...},
{...},
{...}
]Dartأي قائمة Posts.
نوع البيانات القادم
List<dynamic>Dartالتحويل اليدوي
List<PostModel> posts = [];
for (var item in response.data) {
posts.add(
PostModel.fromJson(item),
);
}Dartماذا يحدث؟
العنصر الأول:
{
'id':1,
...
}Dart↓
PostModelDartالعنصر الثاني:
{
'id':2,
...
}Dart↓
PostModelDartفي النهاية:
List<PostModel>Dartالطريقة الاحترافية باستخدام map
بدلاً من loop:
List<PostModel> posts =
response.data.map((item) {
return PostModel.fromJson(item);
}).toList();Dartشرح map
map لا تغير القائمة الأصلية.
بل تنشئ قائمة جديدة.
مثال:
[1,2,3]Dart↓
['1','2','3']Dartفي حالتنا:
JSONDart↓
PostModelDartلماذا نستخدم toList؟
لأن map تعيد:
IterableDartوليس:
ListDartلذلك نكتب:
.toList()Dartللحصول على:
List<PostModel>Dartالكود النهائي
داخل ApiService:
Future<List<PostModel>> getPosts() async {
Response response = await dio.get(
'https://jsonplaceholder.typicode.com/posts',
);
List<PostModel> posts =
response.data
.map<PostModel>(
(json) => PostModel.fromJson(json),
)
.toList();
return posts;
}Dartلماذا غيرنا نوع Future؟
في البداية كان:
Future<void>Dartالآن:
Future<List<PostModel>>Dartلأن الدالة لم تعد تطبع فقط.
بل تعيد بيانات.
النتيجة النهائية
بدلاً من:
response.data[0]['title']
أصبح لدينا:
posts[0].titleDartوهذه خطوة احترافية جداً لأنها تجعل الكود:
✅ واضح
✅ آمن
✅ قابل للصيانة
✅ مناسب للمشاريع الكبيرة
قبل الانتقال إلى POST Request، أنصح بمرحلة صغيرة لكنها مهمة جداً وهي التعمق في fromJson و toJson و map و List<PostModel> و Iterable بالتفصيل لأن 80% من أخطاء المبتدئين في Flutter API تأتي من سوء فهم هذه المفاهيم، وليس من Dio نفسه.
في هذه المرحلة سنبني تطبيقاً كاملاً بسيطاً يجلب Posts ويعرضها داخل ListView باستخدام Dio و Model فقط، بدون Cubit وبدون Clean Architecture.
هيكل المشروع
lib
│
├── main.dart
│
├── models
│ └── post_model.dart
│
├── services
│ └── api_service.dart
│
└── screens
└── posts_page.dartDartالملف الأول
models/post_model.dart
هذا الملف يمثل البيانات القادمة من السيرفر.
class PostModel {
// رقم المستخدم صاحب المنشور
final int userId;
// رقم المنشور
final int id;
// عنوان المنشور
final String title;
// محتوى المنشور
final String body;
// Constructor لإنشاء كائن جديد من PostModel
PostModel({
required this.userId,
required this.id,
required this.title,
required this.body,
});
/*
fromJson
مسؤول عن تحويل JSON القادم من السيرفر
إلى كائن PostModel
json القادم يكون بالشكل:
{
"userId":1,
"id":1,
"title":"Flutter",
"body":"Learning Dio"
}
*/
factory PostModel.fromJson(
Map<String, dynamic> json,
) {
return PostModel(
userId: json['userId'],
id: json['id'],
title: json['title'],
body: json['body'],
);
}
/*
toJson
العكس تماماً.
يحول الكائن إلى Map
سنحتاجه لاحقاً في POST و PUT
*/
Map<String, dynamic> toJson() {
return {
'userId': userId,
'id': id,
'title': title,
'body': body,
};
}
}Dartالملف الثاني
services/api_service.dart
هذا الملف مسؤول عن الشبكة.
import 'package:dio/dio.dart';
import '../models/post_model.dart';
class ApiService {
/*
إنشاء Dio Object
Dio هو المسؤول عن إرسال واستقبال الطلبات
*/
final Dio dio = Dio();
/*
جلب جميع المنشورات
Future<List<PostModel>>
يعني:
سأعيد مستقبلاً قائمة من المنشورات
*/
Future<List<PostModel>> getPosts() async {
/*
إرسال GET Request
*/
Response response = await dio.get(
'https://jsonplaceholder.typicode.com/posts',
);
/*
response.data
تحتوي البيانات القادمة من السيرفر
نوعها الحالي:
List<dynamic>
*/
/*
تحويل List<dynamic>
إلى
List<PostModel>
*/
List<PostModel> posts =
response.data
.map<PostModel>(
(json) =>
PostModel.fromJson(json),
)
.toList();
return posts;
}
}Dartالملف الثالث
screens/posts_page.dart
هذا الملف سيعرض البيانات للمستخدم.
import 'package:flutter/material.dart';
import '../models/post_model.dart';
import '../services/api_service.dart';
class PostsPage extends StatefulWidget {
const PostsPage({super.key});
@override
State<PostsPage> createState() =>
_PostsPageState();
}
class _PostsPageState
extends State<PostsPage> {
/*
إنشاء ApiService
*/
final ApiService apiService =
ApiService();
/*
قائمة المنشورات
*/
List<PostModel> posts = [];
/*
متغير التحميل
*/
bool isLoading = false;
@override
void initState() {
super.initState();
getPosts();
}
/*
جلب البيانات من السيرفر
*/
Future<void> getPosts() async {
/*
تشغيل اللودينغ
*/
setState(() {
isLoading = true;
});
try {
/*
انتظار البيانات
*/
posts =
await apiService.getPosts();
} catch (e) {
print(e);
}
/*
إيقاف اللودينغ
*/
setState(() {
isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text(
'Posts',
),
),
body: isLoading
? const Center(
child:
CircularProgressIndicator(),
)
: ListView.builder(
itemCount: posts.length,
itemBuilder:
(context, index) {
PostModel post =
posts[index];
return Card(
margin:
const EdgeInsets.all(
10,
),
child: Padding(
padding:
const EdgeInsets.all(
16,
),
child: Column(
crossAxisAlignment:
CrossAxisAlignment
.start,
children: [
Text(
post.title,
style:
const TextStyle(
fontSize: 18,
fontWeight:
FontWeight.bold,
),
),
const SizedBox(
height: 10,
),
Text(
post.body,
),
const SizedBox(
height: 10,
),
Text(
'ID : ${post.id}',
),
],
),
),
);
},
),
);
}
}Dartالملف الرابع
main.dart
import 'package:flutter/material.dart';
import 'screens/posts_page.dart';
void main() {
runApp(
const MyApp(),
);
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner:
false,
home: const PostsPage(),
);
}
}Dartماذا سيتعلم الطالب من هذا التطبيق؟
من جهة Flutter
✅ StatefulWidget
✅ initState
✅ setState
✅ ListView.builder
✅ CircularProgressIndicator
✅ Card
✅ Future
✅ async / await
من جهة Dio
✅ Dio Object
✅ GET Request
✅ Response
✅ response.data
✅ تحويل JSON
من جهة Models
✅ Model
✅ fromJson
✅ toJson
✅ List<PostModel>
نقطة تعليمية مهمة جداً
التمرين 1
اعرض فقط:
post.titleDartبدون body.
التمرين 2
اعرض:
post.userIdDartداخل Card.
التمرين 3
اطبع:
response.runtimeTypeDartو
response.data.runtimeTypeDartلفهم الأنواع.
التمرين 4
اجعل عدد العناصر:
posts.take(10).toList()Dartثم اعرض أول 10 فقط.
التمرين 5
أضف زر Refresh واستدعِ:
getPosts()Dartمرة أخرى.
بعد أن يفهم الطالب هذا المشروع بالكامل ننتقل للمرحلة التالية وهي POST Request + إرسال البيانات إلى السيرفر + فهم Request Body و Headers و Content-Type بالتفصيل الاحترافي.