import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:second_app/contact.dart';
import 'package:second_app/products.dart';
import 'colors.dart';
import 'package:http/http.dart';
class HomaPage extends StatefulWidget {
HomaPage({super.key});
@override
State<HomaPage> createState() => _HomaPageState();
}
class _HomaPageState extends State<HomaPage> {
final userNameController = TextEditingController();
final formKey = GlobalKey<FormState>();
String? userName;
Future<List> getData() async {
var response = await get(Uri.parse("https://fakestoreapi.com/products"));
var resBody = jsonDecode(response.body);
return resBody;
}
@override
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("Fake Store")),
drawer: Drawer(),
body: FutureBuilder<List>(
future: getData(),
builder: (context , Snapshot){
if(Snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator.adaptive(),);
}
return Column(
children: [
Expanded(
child: GridView.builder(
padding: EdgeInsets.all(10),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // عدد الأعمدة
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 0.7,
),
itemCount: Snapshot.data!.length,
itemBuilder: (context, i) {
final item = Snapshot.data![i];
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(7),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// الصورة
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.vertical(
top: Radius.circular(8),
),
child: Image.network(
item['image'],
width: double.infinity,
fit: BoxFit.cover,
),
),
),
// النصوص
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['title'],
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
Text(
item['description'],
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: TextStyle(),
),
SizedBox(height: 4),
Text(
"\$${item['price']}",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
);
},
),
),
],
);
}),
);
}
}
Dartما الذي يفعله الكود؟
- يجلب منتجات من API عام:
https://fakestoreapi.com/products. - يعرضها في Grid (شبكة بطاقات) تحتوي صورة، عنوان، وصف، وسعر.
- يعتمد على
FutureBuilder<List>لربط نتيجة الطلب غير المتزامن (Future) بواجهة Flutter.
- اخترت StatefulWidget لأن عندك حالة (جلب بيانات لاحقًا).
- فيه حقول:
userNameController,formKey,userName(حالياً غير مستخدمة في العرض).
- دالة الجلب:
Future<List> getData() async { var response = await get(Uri.parse("https://fakestoreapi.com/products")); var resBody = jsonDecode(response.body); return resBody; }ترجعFuture<List>(القائمة القادمة من الـ API).
يُفضّل إضافة التحقق من
statusCodeوالتعامل مع الأخطاء، وكتابة النوع بشكل أدق:Future<List<Map<String, dynamic>>>.
3) الواجهة العامة
return Scaffold(
appBar: AppBar(title: Text("Fake Store")),
drawer: Drawer(),
body: FutureBuilder<List>(
future: getData(),
builder: (context, Snapshot) {
if (Snapshot.connectionState == ConnectionState.waiting) {
return Center(child: CircularProgressIndicator.adaptive());
}
return Column(
children: [
Expanded(
child: GridView.builder(
...
itemCount: Snapshot.data!.length,
itemBuilder: (context, i) {
final item = Snapshot.data![i];
...
},
),
),
],
);
},
),
);
Dartشرح FutureBuilder بالكامل (النجمة هنا ⭐)
FutureBuilder<T> هو ودجت يبني نفسه بناءً على حالة Future (طلب غير متزامن) ويعطيك AsyncSnapshot<T> داخل builder. أهم النقاط:
1) الخصائص الأساسية
future: الـ Future الذي تنتظر نتيجته (مثلاًgetData()).builder: دالة تُستدعى في كل تغيير بالحالة (قبل وأثناء وبعد اكتمال الـ Future). تستقبل:BuildContextAsyncSnapshot<T> snapshot(في كودك مسميهSnapshot)
2) ما هو AsyncSnapshot؟
يحمل حالة البيانات في لحظة البناء:
connectionState: حالة الاتصال/التنفيذ:ConnectionState.none: لا يوجد Future مرتبط (أو future=null).ConnectionState.waiting: الـ Future قيد التنفيذ (نُظهر Loading).ConnectionState.active: (أكثر معStreamBuilder؛ نادرًا مع Future).ConnectionState.done: الـ Future اكتمل (نجاح/فشل).
hasDataو**data**: هل هناك بيانات؟ وما هي؟hasErrorو**error**: هل حدث خطأ؟ وما رسالته؟
3) دورة الحياة والسلوك
- عند تمرير
future: getData(): يتم استدعاؤه، ويبدأ البناء:- في البداية:
waiting→ اعرض مؤشّر تحميل. - عند اكتمال الـ Future:
- لو نجح:
done,hasData=true→ اعرض البيانات. - لو فشل:
done,hasError=true→ اعرض رسالة خطأ/زر إعادة محاولة.
- لو نجح:
- في البداية:
- تنبّه مهم: استدعاء
getData()داخلbuildيعني أنها ستُستدعى كل مرة يُعاد بناء الودجت (قد يُعيد Flutter البناء لأسباب عديدة)، مما يسبب طلبات متكررة. الحل الأفضل: خزّن الـ Future مرة واحدة فيinitState، ومرّره للـFutureBuilder.
4) ميزات قوية لـ FutureBuilder
- يفصل منطق الحالات (Loading / Data / Error) في مكان واحد أنيق.
- إعادة البناء تلقائيًا عند اكتمال الـ Future.
initialData: يمكنك توفير قيمة مبدئية لعرض Placeholder قبل وصول النتيجة.- التعامل الآمن مع الأخطاء: عبر
snapshot.hasError. - إدارة بسيطة دون setState: لا تحتاج
setStateلتحديث الواجهة عند اكتمال الـ Future؛FutureBuilderيتكفّل بهذا.
5) أفضل الممارسات مع FutureBuilder
- خزّن الـ Future في متغير:
late Future<List<Map<String, dynamic>>> _future; @override void initState() { super.initState(); _future = getData(); }ثم:FutureBuilder(future: _future, builder: ...);لتفادي إعادة الطلب عند كلbuild. - تحقق من الأخطاء:
if (snapshot.hasError) { ... } - تحقق من البيانات قبل الوصول إليها:
if (!snapshot.hasData || snapshot.data!.isEmpty) { ... } - لا تضع منطق ثقيل داخل
builder؛ اجعله لعرض الحالة فقط. - إذا احتجت تحديث البيانات (Refresh)، أعد تعيين
_future = getData(); setState(() {});.
متى أستخدم
StreamBuilderبدلFutureBuilder؟
عندما لديك تدفق مستمر من البيانات (WebSocket, Firestore streams, …) وليس عملية واحدة وتنتهي.
شرح بقية الواجهة
Grid & Card
GridView.builder: إنشاء شبكة عناصر بكفاءة (يبني العناصر عند الحاجة).SliverGridDelegateWithFixedCrossAxisCount: تحدد عدد الأعمدة، المسافات، ونسبة البُعد (Aspect ratio).itemBuilder: يبني بطاقة لكل منتج:Card+ClipRRect: لتطبيق زوايا مستديرة على الصورة.Image.network: تحميل صورة من رابط (يستحسن إضافةerrorBuilderوloadingBuilder).TextمعmaxLinesوoverflowلتفادي تمدد النص.- السعر:
"\$${item['price']}"(هروب الـ$وInterpolation للـ double).
ملاحظات وتحسينات سريعة على كودك
- عندك سطرين
@overrideمتتالين فوقbuild— احذف التكرار. - تسمية الكلاس:
HomaPageيبدو خطأ مطبعي (ربماHomePage؟). FutureBuilder<List>: يفضَّل النوع التفصيلي:FutureBuilder<List<Map<String, dynamic>>>.- استدعاء
getData()داخلbuildيؤدي لإعادة الطلب — خزّنه فيinitState. - تحقّق من الأخطاء وكود الحالة قبل
jsonDecode. - أضف حالات:
- Error UI: عند
snapshot.hasError. - Empty State: عند عدم وجود بيانات.
- Error UI: عند
- في الصور:
- أضف
errorBuilderوloadingBuilderلتحسين التجربة.
- أضف
- استخدم
constحيث يمكن (Performance بسيطة). - سمِّ http:
import 'package:http/http.dart' as http;واستخدمhttp.get.
نسخة مُحسّنة مقتضبة (تعالج النقاط أعلاه)
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
late Future<List<Map<String, dynamic>>> _future;
Future<List<Map<String, dynamic>>> getData() async {
final uri = Uri.parse("https://fakestoreapi.com/products");
final response = await http.get(uri);
if (response.statusCode >= 200 && response.statusCode < 300) {
final decoded = jsonDecode(response.body);
return List<Map<String, dynamic>>.from(decoded);
} else {
throw Exception("HTTP ${response.statusCode}");
}
}
@override
void initState() {
super.initState();
_future = getData(); // لا نعيد الطلب في كل build
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text("Fake Store")),
drawer: const Drawer(),
body: FutureBuilder<List<Map<String, dynamic>>>(
future: _future,
builder: (context, snapshot) {
// حالة التحميل
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator.adaptive());
}
// حالة الخطأ
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('فشل التحميل: ${snapshot.error}'),
const SizedBox(height: 8),
ElevatedButton(
onPressed: () {
setState(() => _future = getData());
},
child: const Text('إعادة المحاولة'),
),
],
),
);
}
// حالة لا توجد بيانات
final data = snapshot.data;
if (data == null || data.isEmpty) {
return const Center(child: Text('لا توجد بيانات لعرضها.'));
}
// عرض الشبكة
return GridView.builder(
padding: const EdgeInsets.all(10),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2, // عدد الأعمدة
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 0.7, // نسبة الطول للعرض للبطاقة
),
itemCount: data.length,
itemBuilder: (context, i) {
final item = data[i];
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(7),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// الصورة
Expanded(
child: ClipRRect(
borderRadius: const BorderRadius.vertical(
top: Radius.circular(8),
),
child: Image.network(
item['image'] ?? '',
width: double.infinity,
fit: BoxFit.cover,
// تحسينات اختيارية:
errorBuilder: (_, __, ___) => const Center(child: Icon(Icons.broken_image)),
loadingBuilder: (context, child, progress) {
if (progress == null) return child;
return const Center(child: CircularProgressIndicator.adaptive());
},
),
),
),
// النصوص
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item['title'] ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
Text(
item['description'] ?? '',
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: 4),
Text(
"\$${item['price']}",
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
);
},
);
},
),
);
}
}
Dartخلاصة سريعة عن FutureBuilder
- يربط واجهتك بحالة Future بدون ما تدير
setStateبنفسك. - يتعامل مع 3 حالات أساسية: التحميل، الخطأ، البيانات.
- استخدم
snapshot.connectionState,snapshot.hasError,snapshot.hasData. - لا تنادِ
getData()داخلbuildمباشرة؛ خزّنه في متغيرFutureفيinitState. - لو احتجت تحديث/تجديد البيانات، أعد تعيين الـ Future داخل
setState.