تمرير البيانات بين الشاشات
تمرير البيانات بين الشاشات
تحتاج كل تطبيقات Flutter الحقيقية تقريباً إلى تمرير المعلومات من شاشة إلى أخرى. تحتاج صفحة قائمة المنتجات إلى إرسال معرّف المنتج إلى صفحة التفاصيل؛ نموذج تسجيل الدخول يجب أن يسلّم رمز المستخدم إلى الشاشة الرئيسية؛ شاشة الإعدادات يجب أن تُعيد السمة التي اختارها المستخدم إلى الشاشة المستدعِية. تمنحك Flutter آليتين نظيفتين لذلك: تمرير البيانات للأمام عبر معاملات المُنشئ (constructor) عند الدفع إلى مسار، وإعادة البيانات للخلف عبر Navigator.pop() عند الخروج من مسار.
Navigator (Navigator 1.0). تنطبق نفس مفاهيم تمرير البيانات على واجهة برمجة التطبيقات التصريحية Router/GoRouter، والتي ستُغطَّى في درس لاحق.تمرير البيانات للأمام إلى شاشة جديدة
عند دفع مسار جديد باستخدام Navigator.push()، تُنشئ نسخة من ودجت الوجهة مباشرةً. أنظف طريقة لإرسال البيانات هي إضافة معاملات مُنشئ مطلوبة إلى ذلك الودجت. يملؤها المستدعي عند وقت الدفع، وتقرأها الوجهة من widget.fieldName (في StatefulWidget) أو مباشرةً من حقول المُنشئ (في StatelessWidget).
مثال: تمرير منتج إلى شاشة التفاصيل
// 1. تعريف كلاس بيانات بسيط
class Product {
final int id;
final String name;
final double price;
const Product({required this.id, required this.name, required this.price});
}
// 2. شاشة الوجهة تقبل البيانات عبر المُنشئ
class ProductDetailScreen extends StatelessWidget {
final Product product;
const ProductDetailScreen({super.key, required this.product});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(product.name)),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('المعرف: ${product.id}'),
Text('السعر: \$${product.price.toStringAsFixed(2)}'),
],
),
),
);
}
}
// 3. المستدعي يدفع المسار ويمرر البيانات
void _openProduct(BuildContext context, Product product) {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => ProductDetailScreen(product: product),
),
);
}
هذا النهج آمن من حيث الأنواع: يفرض مترجم Dart تقديم جميع الحقول المطلوبة. لا يوجد بحث بمفاتيح نصية، ولا تحويل أنواع (casting)، ولا خطر تمرير نوع خاطئ.
freezed / equatable). تجنب تمرير Map<String, dynamic> خام بين الشاشات — يفقد أمان الأنواع ويصعب إعادة هيكلته مع نمو شكل البيانات.إعادة البيانات إلى الشاشة المستدعِية
في بعض الأحيان تجمع الشاشة الجديدة معلومات (تاريخ محدد، قيمة منطقية للتأكيد، نتيجة نموذج) يحتاجها المستدعي بعد أن يتنقل المستخدم للخلف. تقبل Navigator.pop() قيمة نتيجة اختيارية. من جانب المستدعي، تُعيد Navigator.push() Future يحلّ بتلك النتيجة عند إغلاق المسار.
مثال: إعادة لون محدد من شاشة الاختيار
// شاشة الاختيار: تغلق مع اللون المختار
class ColorPickerScreen extends StatelessWidget {
const ColorPickerScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('اختر لوناً')),
body: Column(
children: [
ListTile(
title: const Text('أحمر'),
onTap: () => Navigator.pop(context, 'red'),
),
ListTile(
title: const Text('أخضر'),
onTap: () => Navigator.pop(context, 'green'),
),
ListTile(
title: const Text('أزرق'),
onTap: () => Navigator.pop(context, 'blue'),
),
],
),
);
}
}
// الشاشة المستدعِية: تنتظر الـ Future الذي تُعيده Navigator.push()
class HomeScreen extends StatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
String _selectedColor = 'لا شيء';
Future<void> _openColorPicker() async {
final String? result = await Navigator.push<String>(
context,
MaterialPageRoute(builder: (context) => const ColorPickerScreen()),
);
// النتيجة تكون null إذا ضغط المستخدم زر الرجوع
// دون استدعاء Navigator.pop(context, value)
if (result != null) {
setState(() {
_selectedColor = result;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('الرئيسية')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('اللون المختار: $_selectedColor'),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _openColorPicker,
child: const Text('اختر لوناً'),
),
],
),
),
);
}
}
null قبل استخدامها. يمكن للمستخدم إغلاق المسار المدفوع بالضغط على زر رجوع الجهاز، أو سهم الرجوع في AppBar، أو بالسحب (على iOS). في كل هذه الحالات يُستدعى Navigator.pop() دون قيمة، لذا يحلّ الـ Future بـ null. إهمال هذا الأمر مصدر شائع لأخطاء Null check operator used on a null value.تمرير البيانات مع المسارات المسماة
عند استخدام المسارات المسماة (المعرَّفة في MaterialApp.routes)، لا يمكنك تمرير معاملات المُنشئ مباشرةً. استخدم بدلاً من ذلك Navigator.pushNamed() مع معامل arguments واسترجعها داخل الوجهة عبر ModalRoute.of(context)!.settings.arguments.
تمرير البيانات عبر المسارات المسماة
// إرسال البيانات باستخدام pushNamed
Navigator.pushNamed(
context,
'/product-detail',
arguments: product, // أي كائن Object
);
// استقبال البيانات داخل ودجت الوجهة
@override
Widget build(BuildContext context) {
final product =
ModalRoute.of(context)!.settings.arguments as Product;
return Scaffold(
appBar: AppBar(title: Text(product.name)),
body: Text('السعر: \$${product.price}'),
);
}
arguments ليست آمنة من حيث الأنواع — يجب عليك التحويل يدوياً وأي عدم تطابق يسبب خطأ في وقت التشغيل. للمشاريع الجديدة، يُفضَّل استخدام نهج معاملات المُنشئ أو اعتماد GoRouter (الذي يدعم معاملات extra مكتوبة الأنواع).ملخص
تمنحك Flutter أنماطاً مباشرة وأصيلة لتدفق البيانات بين الشاشات:
- للأمام: أضف معاملات مُنشئ مطلوبة إلى ودجت الوجهة ومرر القيم عند استدعاء
Navigator.push(). - للخلف: استدع
Navigator.pop(context, result)في الوجهة؛ وawait Navigator.push()في المستدعي لاستقبال النتيجة. - المسارات المسماة: استخدم معامل
argumentsفيpushNamed()واسترجعه عبرModalRoute.of(context)!.settings.argumentsمع تحويل النوع المناسب. - احرص دائماً على التحقق من أن النتيجة ليست
nullعند انتظار مسار مدفوع — قد ينتقل المستخدم للخلف دون تقديمها.