المعمل 07: ListView.builder، تصحيح الأخطاء، وخدمات الويب وواجهات API
نظرة عامة
يغطي هذا المعمل أربعة مواضيع مترابطة في Flutter تنقلك من عرض قوائم فعّال إلى تطبيقات متصلة بالشبكة. ستتعلم كيفية بناء قوائم ديناميكية عالية الأداء باستخدام ListView.builder ودالة الاستدعاء itemBuilder الخاصة بها، وكيفية تشخيص وإصلاح أخطاء التشغيل (Runtime Errors) في Flutter باستخدام نقاط التوقف (Breakpoints) والمصحح (Debugger)، وكيفية استهلاك واجهات REST API عبر HTTP - وفهم ماهية خدمة الويب (Web Service) وكيفية الاتصال بها من تطبيق Dart/Flutter باستخدام حزمة http.
الأهداف
بنهاية هذا المعمل ستكون قادرًا على:
- شرح الفرق بين
ListView(قائمة عناصر فرعية ثابتة) وListView.builder(كسولة، مدفوعة بالفهرس Index) - استخدام دالة الاستدعاء
itemBuilderبشكل صحيح لبناء عناصر الواجهة حسب الفهرس - تشغيل
ListView.builderمنListحقيقية من كائنات النموذج في Dart - قراءة سجل أخطاء وقت التشغيل في Flutter وتحديد عنصر الواجهة وإطار المكدّس (Stack Frame) المسبب للانهيار
- ضبط نقاط توقف والتنقل خطوة بخطوة عبر الكود في وضع التصحيح (Debug Mode) داخل Android Studio / VS Code
- تقييم قيم المتغيرات عند نقطة توقف لتحديد خطأ قيمة فارغة (null)
- تعريف ما هي خدمة الويب (Web Service) وواجهة API، والتفريق بين المصطلحين
- شرح دورة طلب/استجابة HTTP ودور عميل HTTP (HTTP Client)
- استخدام حزمة
httpفي Dart لجلب بيانات JSON من نقطة نهاية (Endpoint) REST - تحليل استجابة JSON إلى كائنات نموذج Dart مُحدَّدة النوع باستخدام
fromJson - إدارة ثلاث حالات واجهة (تحميل، خطأ، نجاح) داخل
StatefulWidget
المتطلبات الأساسية
- إعداد مشروع Flutter ومعرفة بعناصر الواجهة (المعامل 01-06)
- الإلمام بأساسيات
StatefulWidget، وsetState()، وListView - صياغة أساسية لأصناف وقوائم وصياغة async/await في Dart
خلفية نظرية
1. ListView.builder - قوائم كسولة مدفوعة بالفهرس
لماذا ListView.builder؟
يبني الباني العادي ListView(children: [...]) كل عناصر الواجهة مقدمًا، حتى تلك الموجودة خارج الشاشة. بالنسبة لمجموعات بيانات كبيرة أو ديناميكية، هذا يهدر الذاكرة ويكون بطيئًا.
يبني ListView.builder فقط عناصر الواجهة المرئية حاليًا على الشاشة. عندما يمرر المستخدم، يستدعي Flutter itemBuilder عند الحاجة لكل عنصر جديد يدخل مجال الرؤية.
توقيع الباني (Constructor Signature)
ListView.builder(
itemCount: courses.length, // total number of items
itemBuilder: (BuildContext context, int index) {
return CourseCard(course: courses[index]);
},
)| المعامل | النوع | الوصف |
|---|---|---|
itemCount | int | عدد العناصر التي تحتويها القائمة |
itemBuilder | Widget Function(BuildContext, int) | تُستدعى مرة واحدة لكل عنصر مرئي؛ تستقبل index يبدأ من الصفر |
كيف يستدعي Flutter itemBuilder
عندما يبني Flutter القائمة لأول مرة، فإنه:
- يقرأ
itemCount(مثلًا 5). - يستدعي
itemBuilder(context, 0)- يعرض العنصر 0. - يستدعي
itemBuilder(context, 1)- يعرض العنصر 1. - يستمر حتى
itemCount - 1. - عندما يمرر المستخدم، يستدعي
itemBuilderللعناصر المرئية حديثًا.
flowchart TD
A[إنشاء ListView.builder] --> B[قراءة itemCount = 5]
B --> C[استدعاء itemBuilder بالفهرس 0]
C --> D[استدعاء itemBuilder بالفهرس 1]
D --> E[استدعاء itemBuilder بالفهرس 2]
E --> F[...]
F --> G[الوصول لـ itemCount - توقف]
G --> H{المستخدم يُمرِّر}
H -->|عنصر جديد يدخل نطاق العرض| I[استدعاء itemBuilder للفهرس الجديد]اربط itemCount دائمًا بطول القائمة
// BAD: hard-coded count goes out of sync when the list changes
ListView.builder(
itemCount: 5, // crashes if courses has only 3 items
itemBuilder: (context, index) => CourseCard(course: courses[index]),
)
// GOOD: always matches the actual data
ListView.builder(
itemCount: courses.length,
itemBuilder: (context, index) => CourseCard(course: courses[index]),
)إذا كتبت itemCount: 5 لكن القائمة الفعلية تحتوي على عنصرين فقط، سيرمي Flutter الخطأ RangeError (index): Invalid value: Not in inclusive range 0..1: 2 - انهيار تجاوز نطاق الفهرس (Index Out of Range) الكلاسيكي.
مثال على قائمة ديناميكية
final List<String> courseNames = ['Intro to Dart', 'Flutter Basics', 'State Management'];
ListView.builder(
itemCount: courseNames.length,
itemBuilder: (BuildContext context, int index) {
return ListTile(
leading: CircleAvatar(child: Text('${index + 1}')),
title: Text(courseNames[index]),
);
},
)أضف عنصرًا آخر إلى courseNames وستعرضه القائمة تلقائيًا - لا حاجة لتغيير أي كود في ListView.builder.
استخلاص صنف عنصر واجهة لكل صف
النمط المُوصى به هو استخلاص الصف إلى StatelessWidget خاص به:
// course_card.dart
class CourseCard extends StatelessWidget {
final String name;
const CourseCard({super.key, required this.name});
@override
Widget build(BuildContext context) {
return Card(
child: ListTile(title: Text(name)),
);
}
}
// courses_screen.dart
ListView.builder(
itemCount: courseNames.length,
itemBuilder: (context, index) => CourseCard(name: courseNames[index]),
)هذا أفضل من كتابة return Card(...) مباشرة لأن:
- يمكن اختبار البطاقة بشكل مستقل
- تملك دورة حياة إعادة بناء خاصة بها (تُعاد بناؤها فقط عندما تتغير حالتها الخاصة)
- قابلة لإعادة الاستخدام عبر شاشات متعددة
ListView مع قائمة من كائنات النموذج
في التطبيقات الحقيقية، العناصر هي كائنات نموذج، وليست نصوصًا خامًا:
class Course {
final String name;
final String imageUrl;
Course({required this.name, required this.imageUrl});
}
final List<Course> courses = [
Course(name: 'Flutter Basics', imageUrl: 'https://example.com/flutter.png'),
Course(name: 'Dart Deep Dive', imageUrl: 'https://example.com/dart.png'),
];
ListView.builder(
itemCount: courses.length,
itemBuilder: (context, index) {
final course = courses[index];
return Row(
children: [
Image.network(course.imageUrl, width: 60, height: 60),
const SizedBox(width: 12),
Text(course.name),
],
);
},
)امتلاك List<Course> واحدة بدلًا من قائمتين متوازيتين (List<String> names + List<String> urls) يضمن بقاء البيانات متزامنة تلقائيًا.
2. تصحيح الأخطاء في Flutter (Debugging)
قراءة سجل الأخطاء (وضع التشغيل - Run Mode)
عندما ينهار التطبيق وتظهر الشاشة الحمراء، فإن الخطوة الأولى هي قراءة سجل تبويب Run - وليس التخمين. يعرض السجل:
- رسالة الاستثناء (Exception Message) - مثل
The method was called on null. - اسم عنصر الواجهة الذي تسبب فيه.
- تتبع المكدّس (Stack Trace) - قائمة بأسماء الملفات وأرقام الأسطر من الاستدعاء الأعمق إلى الخارج.
انقر على اسم ملف في تتبع المكدّس للانتقال مباشرة إلى ذلك السطر في المحرر.
flowchart LR
A[شاشة حمراء أو خطأ] --> B[افتح تبويب Run]
B --> C[جد رسالة الاستثناء]
C --> D[جد الملف:السطر في تتبع المكدّس]
D --> E[انقر الرابط لفتح الملف]
E --> F[حدد القيمة الفارغة أو الخاطئة]أنماط الأخطاء الشائعة:
| نص الخطأ | السبب المحتمل |
|---|---|
The method was called on null | متغيّر قيمته null قبل استدعاء طريقة/getter عليه |
RangeError: Invalid value ... index | itemCount أكبر من طول القائمة |
type 'Null' is not a subtype of type 'String' | حقل يُفترض ألا يكون فارغًا وصل بقيمة null من JSON |
ضبط نقطة توقف والدخول في وضع التصحيح
- انقر في الهامش (Gutter) (الحافة اليسرى للمحرر) بجانب السطر الذي تريد فحصه. تظهر نقطة حمراء - هذه هي نقطة التوقف (Breakpoint).
- شغّل التطبيق باستخدام زر Debug (أيقونة الحشرة)، وليس زر Run العادي.
- يعمل Flutter بشكل طبيعي حتى يصل إلى سطر نقطة التوقف الخاص بك، ثم يتوقف مؤقتًا.
- في لوحة Variables، افحص القيم الحالية لكل المتغيرات المحلية.
flowchart TD
A[حدد السطر المشتبه به من السجل] --> B[انقر الهامش ← نقطة توقف]
B --> C[اضغط زر Debug لبدء التطبيق]
C --> D[يعمل التطبيق بشكل طبيعي حتى نقطة التوقف]
D --> E[التنفيذ يتوقف مؤقتًا]
E --> F[افحص لوحة Variables]
F --> G{هل القيمة فارغة أو خاطئة؟}
G -->|نعم| H[تتبّع للخلف حتى مكان ضبط القيمة]
G -->|لا| I[انتقل لنقطة التوقف التالية]
H --> J[جد السبب الجذري في مصدر البيانات / JSON]التنقل خطوة بخطوة عبر الكود
| الزر | الإجراء |
|---|---|
| Resume (▶) | استمر حتى نقطة التوقف التالية |
| Step Over (→) | نفّذ السطر الحالي وانتقل للسطر التالي |
| Step Into (↓) | ادخل داخل الطريقة المُستدعاة في هذا السطر |
| Step Out (↑) | أنهِ الطريقة الحالية وعُد إلى المستدعي |
إيجاد قيمة فارغة (null) عبر التتبع للخلف
سيناريو واقعي شائع: يجلب التطبيق بيانات المستخدم من API ويحاول عرض user.points، لكنه ينهار بسبب قيمة فارغة.
مسار تصحيح الأخطاء:
- يُظهر السجل أن
pointsفارغة (null). ضع نقطة توقف عند السطر الذي يقرأpoints. - شغّل في وضع التصحيح. عند التوقف، مرر المؤشر فوق المتغيّر أو وسّعه في لوحة Variables.
- تأكد أن
pointsتساويnull. - استخدم Ctrl+Click أو Find Usages لتحديد مكان تعيين
points. - ضع نقطة توقف هناك. استأنف التنفيذ. عند التوقف، افحص خريطة JSON.
- قيّم استجابة API الخام - إذا لم تكن
pointsموجودة في JSON، فإن API يُرجع بيانات ناقصة. - الاستنتاج: الخطأ في استجابة API، وليس في كود Dart. تواصل مع مطور الخلفية (Backend).
هذا نمط يعكس تصحيح الأخطاء الاحترافي الواقعي: الخطأ في طبقة البيانات (API)، وقد اكتُشِف عبر تتبع منهجي بنقاط التوقف.
3. خدمات الويب وواجهات API - المفاهيم
ما هي واجهة API؟
API اختصار لـ Application Programming Interface (واجهة برمجة التطبيقات). هي واجهة تسمح لبرنامج بالتواصل مع برنامج آخر برمجيًا (عبر الكود، وليس واجهة رسومية).
GUI → يتفاعل الإنسان مع البرنامج باستخدام الأزرار والقوائم
API → يتفاعل البرنامج مع برنامج آخر باستخدام الكودما هي خدمة الويب (Web Service)؟
خدمة الويب (Web Service) هي برنامج موجود على الويب ومتاح كخدمة لتستخدمها أنظمة أخرى. كل خدمات الويب هي واجهات API، لكن ليست كل واجهات API خدمات ويب - يمكن أن توجد واجهة API بين برنامجين محليين دون أي اتصال بالشبكة.
عمليًا، يُستخدم مصطلحا "API" و"web service" بالتبادل في الصناعة. الفرق التقني هو:
| خدمة الويب (Web Service) | واجهة API | |
|---|---|---|
| يجب أن تكون على الويب | نعم | لا |
| تتبع معيارًا (REST، SOAP) | نعم | غير مطلوب |
| صيغة البيانات | عادة JSON أو XML | أي صيغة |
لماذا تبني الشركات واجهات API؟
- تحقيق الدخل (Monetisation) - شركات مثل Twilio (الرسائل النصية)، وSendGrid (البريد الإلكتروني)، وAmazon تبيع الوصول إلى واجهات API الخاصة بها.
- مشاركة البيانات - تكشف Facebook وTwitter وGoogle Maps واجهات API حتى تستطيع تطبيقات طرف ثالث قراءة البيانات أو نشرها.
- التكامل الداخلي - يحتاج تطبيق موبايل إلى قاعدة بيانات على خادم؛ وتكون واجهة API هي الجسر بينهما.
حالات استخدام واقعية
flowchart LR
App[تطبيق موبايل] -->|طلب HTTP| API[واجهة API على الخادم]
API -->|استجابة HTTP بصيغة JSON| App
API -->|قراءة / كتابة| DB[(قاعدة بيانات)]حالة الاستخدام 1 - خدمة طرف ثالث: تقدّم شركة خدمة مدفوعة (مثل إرسال الرسائل النصية). يستدعي المطورون واجهة API الخاصة بها لإطلاق رسائل نصية من تطبيقاتهم الخاصة.
حالة الاستخدام 2 - الخلفية (Backend) الخاصة بالتطبيق: يحتاج تطبيق اختبارات (Quiz) إلى تخزين النتائج على خادم حتى يرى المستخدمون لوحة المتصدرين (Leaderboard). يستدعي تطبيق الموبايل واجهة API الخاصة بالشركة نفسها والتي تقرأ/تكتب من قاعدة بيانات الخادم.
بروتوكول HTTP
تتواصل خدمات الويب باستخدام بروتوكول HTTP (أو HTTPS). HTTP هو مجموعة من القواعد التي تحكم التواصل بين عميل (Client) وخادم (Server).
- العميل (Client) - البرنامج الذي يُجري الطلب (تطبيق موبايل، متصفح، Postman)
- الخادم (Server) - الجهاز الذي يستضيف البيانات أو الخدمة
العميل الخادم
| |
|--- طلب HTTP ----------->|
| | (يعالج الطلب)
|<-- استجابة HTTP -----------|
| |HTTPS هو بروتوكول HTTP مع تشفير: تُشفَّر البيانات المنقولة بين العميل والخادم، بحيث لا يرى من يعترض حركة المرور سوى نص مشفّر غير مفهوم.
عميل HTTP (HTTP Client)
عميل HTTP هو برنامج أو مكتبة تتيح لك إرسال طلبات HTTP واستقبال الاستجابات. أمثلة:
| الفئة | أمثلة |
|---|---|
| برامج مستقلة | Chrome، Firefox، Postman |
| مكتبة داخل لغة برمجة | حزمة http (Dart/Flutter)، requests (Python)، HttpClient (Java) |
في Flutter، توفّر حزمة http عميل HTTP الذي تحتاجه للتواصل مع خدمة ويب.
4. استهلاك واجهة REST API في Flutter
أضف حزمة http
# pubspec.yaml
dependencies:
flutter:
sdk: flutter
http: ^1.2.0شغّل flutter pub get بعد الحفظ.
إجراء طلب GET
import 'package:http/http.dart' as http;
import 'dart:convert';
Future<void> fetchData() async {
final response = await http.get(
Uri.parse('https://dummyjson.com/posts'),
);
if (response.statusCode == 200) {
final Map<String, dynamic> decoded = json.decode(response.body) as Map<String, dynamic>;
final List<dynamic> data = decoded['posts'] as List<dynamic>;
// parse data...
} else {
throw Exception('Request failed: ${response.statusCode}');
}
}http.get() غير متزامنة (Asynchronous) - تُرجع Future<Response>. استخدم await معها دائمًا.
المعمارية ثلاثية الطبقات مع مصدر بيانات عبر الشبكة
graph TD
subgraph App
UI[عنصر واجهة الشاشة\nStatefulWidget]
BL[طبقة المنطق\nتصفّي وتحوّل]
DL[طبقة البيانات\nاستدعاءات HTTP]
end
API[(REST API\ndummyjson.com)] --> DL
DL --> BL
BL --> UI| الطبقة | الملف | المسؤولية |
|---|---|---|
| البيانات | posts_data.dart | fetchPosts() - يستدعي API، يُرجع List<PostModel> |
| المنطق | posts_logic.dart | يصفّي القائمة أو يحدّها أو يحوّلها |
| الواجهة | posts_screen.dart | تمتلك الحالة، تستدعي طبقة البيانات، تمرر النتائج لعناصر الواجهة |
| عنصر الواجهة | post_card.dart | يعرض صفًا واحدًا من المنشورات |
تحليل JSON إلى نموذج
عرّف صنف نموذج بواسطة بانٍ مصنعي (Factory) باسم fromJson:
class PostModel {
final int id;
final String title;
final String body;
const PostModel({required this.id, required this.title, required this.body});
factory PostModel.fromJson(Map<String, dynamic> json) {
return PostModel(
id: json['id'] as int,
title: json['title'] as String,
body: json['body'] as String,
);
}
}ثم حوّل قائمة JSON (ملاحظة: يضع dummyjson.com المنشورات داخل مفتاح posts):
final Map<String, dynamic> decoded = json.decode(response.body) as Map<String, dynamic>;
final List<dynamic> data = decoded['posts'] as List<dynamic>;
final posts = data
.map((item) => PostModel.fromJson(item as Map<String, dynamic>))
.toList();إدارة ثلاث حالات واجهة باستخدام setState
يجب أن تتعامل الشاشة التي تحمّل بيانات من الشبكة مع ثلاث حالات:
stateDiagram-v2
[*] --> Loading: فتح الشاشة
Loading --> Success: وصول البيانات
Loading --> Error: فشل الطلب
Error --> Loading: المستخدم ينقر إعادة المحاولة
Success --> Loading: المستخدم ينقر تحديثclass _PostsScreenState extends State<PostsScreen> {
List<PostModel> _posts = [];
bool _isLoading = false;
String? _error;
Future<void> _loadPosts() async {
setState(() { _isLoading = true; _error = null; });
try {
final posts = await PostsData.fetchPosts();
setState(() { _posts = posts; _isLoading = false; });
} catch (e) {
setState(() { _error = e.toString(); _isLoading = false; });
}
}
@override
Widget build(BuildContext context) {
if (_isLoading) return const Center(child: CircularProgressIndicator());
if (_error != null) return Center(child: Text('Error: $_error'));
return ListView.builder(
itemCount: _posts.length,
itemBuilder: (context, index) => PostCard(post: _posts[index]),
);
}
}يُطلق كل استدعاء لـ setState() عملية إعادة بناء واحدة بالضبط بالقيم المُحدَّثة.
لماذا لا نستدعي API مباشرة داخل build()؟
يمكن استدعاء build() مرات عديدة. وضع http.get() داخل build() سيُطلِق طلب شبكة جديد مع كل إعادة بناء - وهذا يهدر النطاق الترددي (Bandwidth) ويسبب حلقات لا نهائية. بدلًا من ذلك، استدعِها من initState() أو من إجراء يقوم به المستخدم:
@override
void initState() {
super.initState();
_loadPosts(); // runs once when the widget first mounts
}مهام المعمل
المهمة 1: ابنِ قائمة مدفوعة بالفهرس
- أنشئ
List<String>تحتوي على 6 أسماء دورات على الأقل في كود Dart. - اعرضها في
ListView.builderباستخدامListTileلكل عنصر. - اضبط
itemCountعلىcourses.length(وليس رقمًا ثابتًا). - أضف عنصرًا سابعًا إلى القائمة. تأكد أنه يظهر تلقائيًا دون تغيير
ListView.builder.
الناتج المتوقع: قائمة قابلة للتمرير تعكس دائمًا الطول الفعلي للقائمة الأساسية.
المهمة 2: استخلص صنف عنصر واجهة لصف
- استخلص كل صف
ListTileإلىStatelessWidgetباسمCourseCardيستقبل معاملString name. - في
ListView.builder، استبدلListTileالمكتوب مباشرة بـCourseCard(name: courses[index]). - شغّل
flutter analyzeوأصلح أي تحذيرات.
الناتج المتوقع: ملف course_card.dart يحتوي على CourseCard، وملف courses_screen.dart نظيف.
المهمة 3: حاكِ وصحّح انهيارًا بسبب قيمة فارغة (null)
- أنشئ شاشة بسيطة تعرض نتيجة مستخدم من متغيّر
int? userScore. - اترك
userScoreفارغة (null) عمدًا واستدعِ.toString()عليها بطريقة قد تسبب انهيارًا (أو اعرضها عبر دمج النصوص لملاحظة سلامة القيم الفارغة - Null Safety). - اقرأ سجل تبويب Run وحدد السطر المسبب للمشكلة.
- ضع نقطة توقف قبل ذلك السطر وشغّل في وضع التصحيح.
- افحص
userScoreفي لوحة Variables. تأكد أنهاnull. - أصلح المشكلة بتوفير قيمة افتراضية:
userScore ?? 0.
الناتج المتوقع: تعرض الشاشة 0 بدلًا من الانهيار.
المهمة 4: اجلب منشورات من واجهة REST API
- أضف
http: ^1.2.0إلىpubspec.yamlوشغّلflutter pub get. - أنشئ
PostModelبالحقولidوtitleوbodyوبانٍ مصنعيfromJson. - أنشئ صنف
PostsDataبطريقة ثابتةfetchPosts()تستدعيhttps://dummyjson.com/posts. - أنشئ
PostsScreen- وهوStatefulWidgetبمتغيرات حالة_isLoadingو_errorو_posts. - استدعِ
fetchPosts()داخلinitState(). - اعرض
CircularProgressIndicatorأثناء التحميل، ورسالة خطأ عند الفشل، وListView.builderعند النجاح.
الناتج المتوقع: تعرض الشاشة مؤشر تحميل دائري، ثم قائمة بعناوين المنشورات المجلوبة من الإنترنت.
المهمة 5: استخلص PostCard وأضف استرجاعًا من الأخطاء
- استخلص صف القائمة إلى
StatelessWidgetباسمPostCardيستقبلPostModel. - أضف زر إعادة محاولة إلى حالة الخطأ يستدعي
_loadPosts()مرة أخرى. - أضف أيقونة تحديث إلى
AppBarتعيد تحميل القائمة عند النقر عليها. - شغّل
flutter analyze- أصلح كل المشكلات.
الناتج المتوقع: كود معياري (Modular) بملفات منفصلة للنموذج، والبيانات، والمنطق، والشاشة، وعنصر الواجهة. صفر مشكلات من المُحلِّل (Analyzer).
الملخص
| المفهوم | الفكرة الأساسية |
|---|---|
ListView.builder | يبني فقط عناصر الواجهة المرئية بكسل (Lazy)؛ اضبط itemCount دائمًا على list.length |
itemBuilder | دالة يستدعيها Flutter لكل عنصر مرئي؛ تستقبل context وindex |
| صنف عنصر الواجهة للصفوف | مُفضَّل على البناء المباشر - دورة حياة خاصة، قابل لإعادة الاستخدام، قابل للاختبار بشكل مستقل |
| مصحح Flutter (Debugger) | ضع نقاط توقف، شغّل في وضع التصحيح، افحص لوحة Variables لإيجاد القيم الفارغة |
| تتبع المكدّس (Stack Trace) | يشير مباشرة إلى الملف والسطر المسبب للانهيار - اقرأه دائمًا أولًا |
| API | واجهة برمجة التطبيقات - عقد برمجي بين نظامين |
| خدمة الويب (Web Service) | واجهة API تعيش على الويب وتتواصل عبر HTTP/HTTPS |
| طلب/استجابة HTTP | يرسل العميل طلبًا إلى خادم؛ ويرد الخادم باستجابة ورمز حالة (Status Code) |
حزمة http | مكتبة Dart توفّر عميل HTTP - استخدم http.get() لجلب البيانات |
بانٍ مصنعي fromJson | يحوّل خريطة (Map) JSON خامًا إلى كائن نموذج Dart مُحدَّد النوع |
| ثلاث حالات واجهة | تحميل (CircularProgressIndicator)، خطأ (رسالة + إعادة محاولة)، نجاح (قائمة) |
initState() | المكان الصحيح لإطلاق أول استدعاء شبكة - يعمل مرة واحدة عند تركيب عنصر الواجهة |