تعمق في ودجات Material
استكشاف ودجات 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 مع أعمدة اسم المنتج والسعر والتقييم.