أساسيات ودجات Flutter

تعمق في ودجات Material

50 دقيقة الدرس 12 من 18

استكشاف ودجات Material Design

يأتي Flutter مع مجموعة غنية من ودجات Material Design تتجاوز الأساسيات. في هذا الدرس ستستكشف الرقائق والتلميحات والشارات ومؤشرات التقدم والخطوات وجداول البيانات وودجات التوسيع. هذه المكونات تساعدك على بناء واجهات مصقولة وغنية بالميزات دون الاعتماد على حزم خارجية.

ودجات الرقائق (Chip)

الرقائق هي عناصر مدمجة تمثل سمات أو إجراءات أو فلاتر. يوفر Flutter خمسة أنواع من الرقائق كل منها مصمم لحالة استخدام محددة.

رقاقة أساسية

رقاقة للقراءة فقط تعرض معلومات مثل وسم أو تسمية.

رقاقة أساسية

Chip(
  avatar: const CircleAvatar(
    backgroundColor: Colors.blue,
    child: Text('F'),
  ),
  label: const Text('Flutter'),
  onDeleted: () {
    debugPrint('تم حذف الرقاقة');
  },
  deleteIcon: const Icon(Icons.close, size: 18),
)

ActionChip

رقاقة تُفعّل إجراء عند الضغط عليها مشابهة لزر مدمج.

مثال ActionChip

ActionChip(
  avatar: const Icon(Icons.directions_car, size: 18),
  label: const Text('الحصول على الاتجاهات'),
  onPressed: () {
    debugPrint('فتح الخرائط...');
  },
)

FilterChip

رقاقة مع علامة اختيار تبدّل فلتر بين التشغيل والإيقاف. مفيدة لسيناريوهات الاختيار المتعدد.

FilterChip مع الحالة

class FilterChipExample extends StatefulWidget {
  const FilterChipExample({super.key});

  @override
  State<FilterChipExample> createState() => _FilterChipExampleState();
}

class _FilterChipExampleState extends State<FilterChipExample> {
  final Map<String, bool> _filters = {
    'Dart': false,
    'Flutter': true,
    'Firebase': false,
  };

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 8,
      children: _filters.entries.map((entry) {
        return FilterChip(
          label: Text(entry.key),
          selected: entry.value,
          onSelected: (bool selected) {
            setState(() => _filters[entry.key] = selected);
          },
          selectedColor: Colors.blue.shade100,
          checkmarkColor: Colors.blue,
        );
      }).toList(),
    );
  }
}

ChoiceChip

مشابهة لزر الراديو -- يمكن اختيار رقاقة واحدة فقط في المرة الواحدة.

ChoiceChip للاختيار الفردي

class ChoiceChipExample extends StatefulWidget {
  const ChoiceChipExample({super.key});

  @override
  State<ChoiceChipExample> createState() => _ChoiceChipExampleState();
}

class _ChoiceChipExampleState extends State<ChoiceChipExample> {
  int _selectedIndex = 0;
  final List<String> _sizes = ['صغير', 'متوسط', 'كبير'];

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 8,
      children: List.generate(_sizes.length, (index) {
        return ChoiceChip(
          label: Text(_sizes[index]),
          selected: _selectedIndex == index,
          onSelected: (bool selected) {
            setState(() => _selectedIndex = index);
          },
        );
      }),
    );
  }
}

InputChip

رقاقة تمثل معلومات معقدة مثل جهة اتصال. يمكن تحديدها وحذفها والضغط عليها.

InputChip للوسوم

InputChip(
  avatar: const CircleAvatar(
    backgroundImage: NetworkImage('https://example.com/avatar.png'),
  ),
  label: const Text('أحمد'),
  selected: true,
  onSelected: (bool selected) {
    debugPrint('محدد: \$selected');
  },
  onDeleted: () {
    debugPrint('تمت إزالة أحمد');
  },
  onPressed: () {
    debugPrint('تم الضغط على أحمد');
  },
)
نصيحة: استخدم Wrap كعنصر أب لقوائم الرقائق. يقوم تلقائياً بنقل الرقائق إلى السطر التالي عند نفاد المساحة في الصف وهو بالضبط سلوك التخطيط الذي تريده للمكونات الشبيهة بالوسوم.

Tooltip

Tooltip يعرض تسمية نصية قصيرة عندما يضغط المستخدم مطولاً أو يمرر فوق ودجة. تستخدم أدوات الوصول التلميحات لوصف عناصر واجهة المستخدم.

مثال Tooltip

Tooltip(
  message: 'إضافة عنصر جديد إلى قائمتك',
  preferBelow: false,
  showDuration: const Duration(seconds: 2),
  child: IconButton(
    icon: const Icon(Icons.add),
    onPressed: () {
      debugPrint('تم الضغط على إضافة');
    },
  ),
)

Badge

ودجة Badge (قُدمت في Flutter 3.7) تُركّب تسمية صغيرة على ودجة أخرى وتُستخدم عادة لعدد الإشعارات.

شارة على أيقونة

Badge(
  label: const Text('3'),
  child: const Icon(Icons.notifications, size: 30),
)

// شارة بدون تسمية (مؤشر نقطة)
Badge(
  smallSize: 8,
  child: const Icon(Icons.mail, size: 30),
)

مؤشرات التقدم

يوفر Flutter مؤشرَي تقدم لعرض حالة التحميل أو العمليات.

CircularProgressIndicator

تقدم دائري

// غير محدد (يدور)
const CircularProgressIndicator()

// محدد (يعرض تقدماً محدداً)
CircularProgressIndicator(
  value: 0.7, // مكتمل 70%
  strokeWidth: 6,
  backgroundColor: Colors.grey.shade300,
  valueColor: const AlwaysStoppedAnimation<Color>(Colors.blue),
)

LinearProgressIndicator

تقدم خطي

// غير محدد
const LinearProgressIndicator()

// محدد مع تنسيق
LinearProgressIndicator(
  value: 0.45,
  minHeight: 8,
  backgroundColor: Colors.grey.shade200,
  borderRadius: BorderRadius.circular(4),
  valueColor: const AlwaysStoppedAnimation<Color>(Colors.green),
)
ملاحظة: استخدم الشكل غير المحدد (بدون value) عندما لا تعرف كم ستستغرق العملية. استخدم الشكل المحدد (مع value بين 0.0 و 1.0) عندما تستطيع تتبع التقدم مثل رفع ملف.

Stepper

ودجة Stepper توجه المستخدمين عبر سلسلة خطوات مثل معالج نماذج.

مثال Stepper

class StepperExample extends StatefulWidget {
  const StepperExample({super.key});

  @override
  State<StepperExample> createState() => _StepperExampleState();
}

class _StepperExampleState extends State<StepperExample> {
  int _currentStep = 0;

  @override
  Widget build(BuildContext context) {
    return Stepper(
      currentStep: _currentStep,
      onStepContinue: () {
        if (_currentStep < 2) {
          setState(() => _currentStep++);
        }
      },
      onStepCancel: () {
        if (_currentStep > 0) {
          setState(() => _currentStep--);
        }
      },
      onStepTapped: (int step) {
        setState(() => _currentStep = step);
      },
      steps: const [
        Step(
          title: Text('الحساب'),
          content: Text('أدخل بريدك الإلكتروني وكلمة المرور.'),
          isActive: true,
        ),
        Step(
          title: Text('الملف الشخصي'),
          content: Text('أعد معلومات ملفك الشخصي.'),
        ),
        Step(
          title: Text('التأكيد'),
          content: Text('راجع وأرسل بياناتك.'),
        ),
      ],
    );
  }
}

DataTable

DataTable يعرض بيانات جدولية مع الفرز والتحديد وإجراءات الصفوف.

DataTable مع الفرز

class DataTableExample extends StatefulWidget {
  const DataTableExample({super.key});

  @override
  State<DataTableExample> createState() => _DataTableExampleState();
}

class _DataTableExampleState extends State<DataTableExample> {
  bool _sortAscending = true;
  int _sortColumnIndex = 0;

  final List<Map<String, dynamic>> _data = [
    {'name': 'Flutter', 'stars': 162000, 'language': 'Dart'},
    {'name': 'React Native', 'stars': 115000, 'language': 'JavaScript'},
    {'name': 'Kotlin MP', 'stars': 48000, 'language': 'Kotlin'},
  ];

  void _sort(int columnIndex, bool ascending) {
    setState(() {
      _sortColumnIndex = columnIndex;
      _sortAscending = ascending;
      if (columnIndex == 1) {
        _data.sort((a, b) => ascending
            ? a['stars'].compareTo(b['stars'])
            : b['stars'].compareTo(a['stars']));
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      scrollDirection: Axis.horizontal,
      child: DataTable(
        sortColumnIndex: _sortColumnIndex,
        sortAscending: _sortAscending,
        columns: [
          const DataColumn(label: Text('الإطار')),
          DataColumn(
            label: const Text('النجوم'),
            numeric: true,
            onSort: _sort,
          ),
          const DataColumn(label: Text('اللغة')),
        ],
        rows: _data.map((item) {
          return DataRow(cells: [
            DataCell(Text(item['name'])),
            DataCell(Text(item['stars'].toString())),
            DataCell(Text(item['language'])),
          ]);
        }).toList(),
      ),
    );
  }
}

ExpansionTile

ExpansionTile ينشئ عنصر قائمة قابل للطي مع رأس يتوسع ليكشف المحتوى الفرعي.

مثال ExpansionTile

ExpansionTile(
  leading: const Icon(Icons.settings),
  title: const Text('الإعدادات المتقدمة'),
  subtitle: const Text('تكوين خيارات إضافية'),
  initiallyExpanded: false,
  childrenPadding: const EdgeInsets.symmetric(horizontal: 16),
  children: const [
    ListTile(title: Text('حجم التخزين المؤقت'), trailing: Text('256 MB')),
    ListTile(title: Text('المزامنة التلقائية'), trailing: Text('مفعّل')),
    ListTile(title: Text('وضع التصحيح'), trailing: Text('معطّل')),
  ],
)

ExpansionPanelList

لمجموعة لوحات حيث يمكن أن يؤدي توسيع واحدة إلى طي الأخريات اختيارياً.

ExpansionPanelList (أكورديون)

class AccordionExample extends StatefulWidget {
  const AccordionExample({super.key});

  @override
  State<AccordionExample> createState() => _AccordionExampleState();
}

class _AccordionExampleState extends State<AccordionExample> {
  final List<bool> _isExpanded = [false, false, false];

  @override
  Widget build(BuildContext context) {
    return SingleChildScrollView(
      child: ExpansionPanelList(
        expansionCallback: (int index, bool isExpanded) {
          setState(() => _isExpanded[index] = isExpanded);
        },
        children: [
          ExpansionPanel(
            headerBuilder: (context, isExpanded) {
              return const ListTile(title: Text('القسم 1'));
            },
            body: const Padding(
              padding: EdgeInsets.all(16),
              child: Text('محتوى القسم 1.'),
            ),
            isExpanded: _isExpanded[0],
          ),
          ExpansionPanel(
            headerBuilder: (context, isExpanded) {
              return const ListTile(title: Text('القسم 2'));
            },
            body: const Padding(
              padding: EdgeInsets.all(16),
              child: Text('محتوى القسم 2.'),
            ),
            isExpanded: _isExpanded[1],
          ),
          ExpansionPanel(
            headerBuilder: (context, isExpanded) {
              return const ListTile(title: Text('القسم 3'));
            },
            body: const Padding(
              padding: EdgeInsets.all(16),
              child: Text('محتوى القسم 3.'),
            ),
            isExpanded: _isExpanded[2],
          ),
        ],
      ),
    );
  }
}
تحذير: يجب وضع ExpansionPanelList داخل ودجة قابلة للتمرير مثل SingleChildScrollView أو ListView لأنها لا تتعامل مع التجاوز من تلقاء نفسها. نسيان هذا سيسبب أخطاء تخطيط عند توسع اللوحات لتتجاوز المساحة المتاحة.

مثال عملي: محدد الوسوم

محدد وسوم كامل يجمع بين FilterChip مع حقل بحث.

ودجة محدد الوسوم

class TagSelector extends StatefulWidget {
  const TagSelector({super.key});

  @override
  State<TagSelector> createState() => _TagSelectorState();
}

class _TagSelectorState extends State<TagSelector> {
  final List<String> _allTags = [
    'Flutter', 'Dart', 'Firebase', 'iOS',
    'Android', 'Web', 'Desktop', 'Testing',
  ];
  final Set<String> _selectedTags = {};

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        Text(
          'اختر الوسوم (\${_selectedTags.length} محدد)',
          style: Theme.of(context).textTheme.titleMedium,
        ),
        const SizedBox(height: 12),
        Wrap(
          spacing: 8,
          runSpacing: 8,
          children: _allTags.map((tag) {
            final isSelected = _selectedTags.contains(tag);
            return FilterChip(
              label: Text(tag),
              selected: isSelected,
              onSelected: (bool selected) {
                setState(() {
                  if (selected) {
                    _selectedTags.add(tag);
                  } else {
                    _selectedTags.remove(tag);
                  }
                });
              },
            );
          }).toList(),
        ),
      ],
    );
  }
}

ملخص

  • الرقائق -- Chip و ActionChip و FilterChip و ChoiceChip و InputChip للوسوم والفلاتر والإجراءات المدمجة
  • Tooltip -- نص تلميح عند التمرير/الضغط المطول لإمكانية الوصول
  • Badge -- عدد الإشعارات أو مؤشر نقطة مُركّب
  • ProgressIndicator -- مؤشرات تحميل دائرية وخطية (محددة وغير محددة)
  • Stepper -- واجهة معالج متعدد الخطوات موجّه
  • DataTable -- عرض بيانات جدولية مع الفرز والتحديد
  • ExpansionTile / ExpansionPanelList -- أقسام محتوى قابلة للطي

تمرين عملي

ابنِ شاشة فلترة منتجات. في الأعلى استخدم ChoiceChip لاختيار فئة (إلكترونيات، ملابس، كتب). أسفلها استخدم FilterChip لتحديد العلامات التجارية. أضف LinearProgressIndicator يُظهر عدد الفلاتر النشطة من المجموع. اعرض النتائج المفلترة في DataTable مع أعمدة اسم المنتج والسعر والتقييم.