تخطَّ إلى المحتوى

القوائم في Flutter: ListView و ListTile

الدرس 8 من 33 · Flutter

افتح أي تطبيق على هاتفك: المحادثات قائمة، والإشعارات قائمة، والمنتجات قائمة، والإعدادات قائمة. عرض القوائم هو أكثر ما ستفعله في تطبيقات Flutter. في هذا الدرس ستتعلم كيف تعرض القوائم القصيرة والطويلة بكفاءة، وستجمع كل ما تعلمته حتى الآن في مشروع قائمة مهام كاملة.

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 لأن «الأمام» في التطبيق العربي نحو اليسار.

هنا يجتمع كل الكورس: Stateful و setState، و TextField والمتحكم، و Row و Expanded، و ListView.builder:

lib/main.dart
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.

  1. ابنِ شاشة الكورسات، وأضف كورسين جديدين للقائمة.
  2. ابنِ قائمة المهام، وجرّب الإضافة والحذف.
  3. أضف Checkbox في leading لكل مهمة لتعليمها كمنجزة، مع خط في منتصف النص المنجز (TextDecoration.lineThrough). تلميح: حوّل المهمة من نص إلى كلاس فيه title و done.
  4. تحدٍّ: اعرض رسالة «لا توجد مهام، أضف أول مهمة!» عندما تصبح القائمة فارغة.
  • ListView داخل Column بدون Expanded: خطأ unbounded height. لف القائمة بـ Expanded.
  • ListView العادية لقائمة طويلة: تبني كل العناصر مرة واحدة فتبطئ التطبيق. استخدم ListView.builder.
  • تعديل القائمة بدون setState: المهمة تُضاف في الذاكرة لكن الشاشة لا تتحدث.

سؤال

لماذا نفضّل ListView.builder للقوائم الطويلة؟

سؤال

أي خاصية في ListTile تضع عنصراً في نهاية السطر، مثل زر حذف؟

  • ListView قائمة قابلة للتمرير، و ListTile عنصر جاهز بـ leading و title و subtitle و trailing و onTap.
  • ListView.builder تبني العناصر عند الحاجة فقط، وهي الاختيار الصحيح للقوائم الطويلة.
  • ListView.separated تضيف فواصل بين العناصر.
  • اعرض البيانات من قائمة كائنات، وانتقل للتفاصيل بـ Navigator.push.
  • داخل Column لف القائمة بـ Expanded. بهذا أنهيت أساسيات Flutter، وفي الدروس القادمة ستتعمق في كل Widget على حدة.