القوائم في Flutter: ListView و ListTile
افتح أي تطبيق على هاتفك: المحادثات قائمة، والإشعارات قائمة، والمنتجات قائمة، والإعدادات قائمة. عرض القوائم هو أكثر ما ستفعله في تطبيقات Flutter. في هذا الدرس ستتعلم كيف تعرض القوائم القصيرة والطويلة بكفاءة، وستجمع كل ما تعلمته حتى الآن في مشروع قائمة مهام كاملة.
ListView البسيطة
Section titled “ListView البسيطة”ListView مثل Column، لكنها قابلة للتمرير تلقائياً عندما يزيد المحتوى عن الشاشة:
ListView( children: const [ ListTile(leading: Icon(Icons.home), title: Text('الرئيسية')), ListTile(leading: Icon(Icons.settings), title: Text('الإعدادات')), ListTile(leading: Icon(Icons.info), title: Text('عن التطبيق')), ],)تذكّر خطأ Overflow من درس Row و Column: ListView تحل هذه المشكلة للمحتوى الطويل.
ListTile: عنصر القائمة الجاهز
Section titled “ListTile: عنصر القائمة الجاهز”بدلاً من بناء كل عنصر بـ Row و Column يدوياً، يوفر Flutter ListTile بتصميم جاهز ومتناسق:
leading: أيقونة أو صورة في بداية السطر.title: النص الرئيسي.subtitle: نص ثانوي أصغر تحته.trailing: عنصر في نهاية السطر، مثل سهم أو زر.onTap: دالة تُنفَّذ عند الضغط على العنصر.
القوائم الطويلة: ListView.builder
Section titled “القوائم الطويلة: ListView.builder”ماذا لو كانت القائمة 1000 عنصر؟ ListView العادية تبني الألف كلها مرة واحدة، حتى التي لا تظهر على الشاشة! الحل: ListView.builder:
ListView.builder( itemCount: 1000, itemBuilder: (context, index) { return ListTile(title: Text('العنصر رقم ${index + 1}')); },)itemCount: عدد العناصر.itemBuilder: دالة يستدعيها Flutter لبناء العنصر رقمindex(يبدأ من صفر)، فقط عندما يقترب من الظهور على الشاشة.
جرّبنا هذا الكود في اختبار على شاشة بحجم هاتف: من أصل 1000 عنصر، بنى Flutter 11 عنصراً فقط، وهي التي تظهر على الشاشة وما حولها. ومع التمرير يبني الجديد ويتخلص من القديم.
تخيّلها مثل مطعم يطبخ عند الطلب، بدلاً من طبخ ألف وجبة مسبقاً لا يأكلها أحد. لذلك القاعدة: إذا كانت القائمة طويلة أو تأتي من الإنترنت، استخدم ListView.builder دائماً.
عرض بيانات حقيقية من كلاس
Section titled “عرض بيانات حقيقية من كلاس”في التطبيقات الحقيقية، البيانات تكون قائمة من كائنات، كما تعلمت في درس الكلاسات بكورس Dart:
class Course { final String name; final int lessons; final IconData icon;
const Course({required this.name, required this.lessons, required this.icon});}
const courses = [ Course(name: 'HTML', lessons: 9, icon: Icons.code), Course(name: 'CSS', lessons: 9, icon: Icons.palette), Course(name: 'JavaScript', lessons: 16, icon: Icons.bolt), Course(name: 'Dart', lessons: 8, icon: Icons.flutter_dash), Course(name: 'Python', lessons: 16, icon: Icons.terminal),];ثم نعرضها، مع فاصل بين العناصر بـ ListView.separated، والانتقال لصفحة التفاصيل عند الضغط:
class CoursesScreen extends StatelessWidget { const CoursesScreen({super.key});
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('الكورسات (${courses.length})')), body: ListView.separated( itemCount: courses.length, separatorBuilder: (context, index) => const Divider(height: 1), itemBuilder: (context, index) { final course = courses[index]; return ListTile( leading: Icon(course.icon), title: Text(course.name), subtitle: Text('${course.lessons} دروس'), trailing: const Icon(Icons.chevron_left), onTap: () { Navigator.push( context, MaterialPageRoute(builder: (context) => CourseDetails(course: course)), ); }, ); }, ), ); }}النتيجة: شاشة عنوانها «الكورسات (5)»، فيها خمسة عناصر بأيقونة واسم وعدد دروس وسهم، بينها خطوط فاصلة رفيعة. والضغط على «Dart» يفتح شاشة فيها «هذا الكورس فيه 8 دروس». لاحظ أننا استخدمنا سهم chevron_left لأن «الأمام» في التطبيق العربي نحو اليسار.
المشروع: قائمة مهام
Section titled “المشروع: قائمة مهام”هنا يجتمع كل الكورس: Stateful و setState، و TextField والمتحكم، و Row و Expanded، و ListView.builder:
class TodoScreen extends StatefulWidget { const TodoScreen({super.key});
@override State<TodoScreen> createState() => _TodoScreenState();}
class _TodoScreenState extends State<TodoScreen> { final _tasks = <String>['مذاكرة Flutter', 'حل التمارين']; final _controller = TextEditingController();
@override void dispose() { _controller.dispose(); super.dispose(); }
void _addTask() { final text = _controller.text.trim(); if (text.isEmpty) return; setState(() { _tasks.add(text); }); _controller.clear(); }
@override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('مهامي')), body: Column( children: [ Padding( padding: const EdgeInsets.all(8), child: Row( children: [ Expanded(child: TextField(controller: _controller)), IconButton(onPressed: _addTask, icon: const Icon(Icons.add)), ], ), ), Expanded( child: ListView.builder( itemCount: _tasks.length, itemBuilder: (context, index) { return ListTile( title: Text(_tasks[index]), trailing: IconButton( icon: const Icon(Icons.delete), onPressed: () { setState(() { _tasks.removeAt(index); }); }, ), ); }, ), ), ], ), ); }}اكتب «قراءة كتاب» واضغط علامة +، فتظهر المهمة في آخر القائمة ويُمسح الحقل. واضغط أيقونة الحذف بجانب أي مهمة فتختفي.
انتبه لـ Expanded حول ListView: داخل Column، القائمة تحتاج أن تعرف كم مساحة متاحة لها، و Expanded تعطيها كل المساحة المتبقية تحت حقل الإدخال. بدونها يظهر خطأ شهير: Vertical viewport was given unbounded height.
جرّب بنفسك
Section titled “جرّب بنفسك”- ابنِ شاشة الكورسات، وأضف كورسين جديدين للقائمة.
- ابنِ قائمة المهام، وجرّب الإضافة والحذف.
- أضف
Checkboxفيleadingلكل مهمة لتعليمها كمنجزة، مع خط في منتصف النص المنجز (TextDecoration.lineThrough). تلميح: حوّل المهمة من نص إلى كلاس فيهtitleوdone. - تحدٍّ: اعرض رسالة «لا توجد مهام، أضف أول مهمة!» عندما تصبح القائمة فارغة.
أخطاء شائعة
Section titled “أخطاء شائعة”- ListView داخل Column بدون Expanded: خطأ
unbounded height. لف القائمة بـExpanded. - ListView العادية لقائمة طويلة: تبني كل العناصر مرة واحدة فتبطئ التطبيق. استخدم
ListView.builder. - تعديل القائمة بدون setState: المهمة تُضاف في الذاكرة لكن الشاشة لا تتحدث.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
لماذا نفضّل ListView.builder للقوائم الطويلة؟
builder تبني العنصر فقط عندما يقترب من الظهور، فقائمة من 1000 عنصر تبني حوالي عشرة فقط في البداية.
سؤال
أي خاصية في ListTile تضع عنصراً في نهاية السطر، مثل زر حذف؟
leading للبداية، و trailing للنهاية، و subtitle للنص الثانوي تحت العنوان.
الخلاصة
Section titled “الخلاصة”ListViewقائمة قابلة للتمرير، وListTileعنصر جاهز بـleadingوtitleوsubtitleوtrailingوonTap.ListView.builderتبني العناصر عند الحاجة فقط، وهي الاختيار الصحيح للقوائم الطويلة.ListView.separatedتضيف فواصل بين العناصر.- اعرض البيانات من قائمة كائنات، وانتقل للتفاصيل بـ
Navigator.push. - داخل
Columnلف القائمة بـExpanded. بهذا أنهيت أساسيات Flutter، وفي الدروس القادمة ستتعمق في كل Widget على حدة.