أنماط تركيب الودجات
تركيب الودجات في Flutter
فلسفة Flutter الأساسية هي التركيب بدلاً من الوراثة. بدلاً من إنشاء ودجات معقدة من خلال تسلسلات هرمية عميقة للفئات تبني واجهات مستخدم متطورة عن طريق دمج ودجات صغيرة ومركّزة معاً. يغطي هذا الدرس الأنماط الرئيسية لتركيب الودجات بفعالية: استخراج الودجات وأنماط البناء وأنماط الاستدعاء الراجع وإنشاء مكونات قابلة لإعادة الاستخدام.
استخراج الودجات إلى فئات
نمط التركيب الأساسي هو استخراج أجزاء من شجرة الودجات إلى فئات ودجات منفصلة. هذا يبقي كل فئة مركّزة وسهلة الفهم.
قبل: دالة build ضخمة
الكثير في مكان واحد
// سيء: كل شيء مكدس في دالة build واحدة
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('الملف الشخصي')),
body: Column(
children: [
// أكثر من 50 سطراً لقسم الرأس
Container(
padding: const EdgeInsets.all(24),
child: Column(
children: [
const CircleAvatar(radius: 50),
const SizedBox(height: 12),
const Text('أحمد', style: TextStyle(fontSize: 24)),
const Text('مطور Flutter'),
// ... المزيد من الودجات
],
),
),
// أكثر من 50 سطراً لقسم الإحصائيات
// أكثر من 50 سطراً لقسم الإجراءات
],
),
);
}
}
بعد: ودجات مستخرجة
تركيب نظيف
class ProfileScreen extends StatelessWidget {
const ProfileScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('الملف الشخصي')),
body: const Column(
children: [
ProfileHeader(
name: 'أحمد',
title: 'مطور Flutter',
),
ProfileStats(
followers: 1200,
following: 340,
projects: 28,
),
ProfileActions(),
],
),
);
}
}
class ProfileHeader extends StatelessWidget {
final String name;
final String title;
const ProfileHeader({
super.key,
required this.name,
required this.title,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(24),
child: Column(
children: [
const CircleAvatar(radius: 50),
const SizedBox(height: 12),
Text(
name,
style: Theme.of(context).textTheme.headlineSmall,
),
Text(
title,
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
color: Colors.grey,
),
),
],
),
);
}
}
class ProfileStats extends StatelessWidget {
final int followers;
final int following;
final int projects;
const ProfileStats({
super.key,
required this.followers,
required this.following,
required this.projects,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_StatItem(label: 'المتابعون', count: followers),
_StatItem(label: 'يتابع', count: following),
_StatItem(label: 'المشاريع', count: projects),
],
);
}
}
class _StatItem extends StatelessWidget {
final String label;
final int count;
const _StatItem({required this.label, required this.count});
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
count.toString(),
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
),
),
Text(label, style: const TextStyle(color: Colors.grey)),
],
);
}
}
نمط البناء (Builder Pattern)
يستخدم Flutter نمط البناء على نطاق واسع. البناء هو استدعاء راجع يستقبل السياق ويعيد ودجة. هذا النمط يتيح البناء الكسول والتخطيطات المتجاوبة ومعالجة البيانات غير المتزامنة.
LayoutBuilder
LayoutBuilder يوفر قيود العنصر الأب حتى تتمكن من بناء تخطيطات متجاوبة.
تخطيط متجاوب مع LayoutBuilder
class ResponsiveGrid extends StatelessWidget {
const ResponsiveGrid({super.key});
@override
Widget build(BuildContext context) {
return LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (constraints.maxWidth > 900) {
return _buildWideLayout();
} else if (constraints.maxWidth > 600) {
return _buildMediumLayout();
} else {
return _buildNarrowLayout();
}
},
);
}
Widget _buildWideLayout() {
return const Row(
children: [
Expanded(flex: 1, child: Placeholder()),
Expanded(flex: 2, child: Placeholder()),
Expanded(flex: 1, child: Placeholder()),
],
);
}
Widget _buildMediumLayout() {
return const Row(
children: [
Expanded(child: Placeholder()),
Expanded(child: Placeholder()),
],
);
}
Widget _buildNarrowLayout() {
return const Column(
children: [Placeholder(), Placeholder()],
);
}
}
FutureBuilder
FutureBuilder يبني ودجات بناءً على حالة Future. يتعامل مع حالات التحميل والخطأ والبيانات تلقائياً.
مثال FutureBuilder
class UserProfile extends StatelessWidget {
const UserProfile({super.key});
Future<Map<String, String>> _fetchUser() async {
await Future.delayed(const Duration(seconds: 2));
return {'name': 'أحمد', 'email': 'ahmed@example.com'};
}
@override
Widget build(BuildContext context) {
return FutureBuilder<Map<String, String>>(
future: _fetchUser(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(
child: CircularProgressIndicator(),
);
}
if (snapshot.hasError) {
return Center(
child: Text('خطأ: \${snapshot.error}'),
);
}
final user = snapshot.data!;
return ListTile(
leading: const CircleAvatar(child: Icon(Icons.person)),
title: Text(user['name']!),
subtitle: Text(user['email']!),
);
},
);
}
}
future لـ FutureBuilder في دالة build لـ StatelessWidget بدون تخزين Future مؤقتاً. كل إعادة بناء تنشئ Future جديد مما يسبب إعادات بناء لا نهائية. إما استخدم StatefulWidget وخزّن Future في initState أو استخدم حقل late final.StreamBuilder
StreamBuilder يعمل مثل FutureBuilder لكن لتدفقات مستمرة من البيانات.
مثال StreamBuilder
class CounterStream extends StatelessWidget {
const CounterStream({super.key});
Stream<int> _countStream() async* {
for (int i = 1; i <= 10; i++) {
await Future.delayed(const Duration(seconds: 1));
yield i;
}
}
@override
Widget build(BuildContext context) {
return StreamBuilder<int>(
stream: _countStream(),
initialData: 0,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('خطأ: \${snapshot.error}');
}
return Center(
child: Text(
'العدد: \${snapshot.data}',
style: const TextStyle(fontSize: 48),
),
);
},
);
}
}
أنماط الاستدعاء الراجع
الاستدعاءات الراجعة مثل onPressed و onChanged و onSubmitted هي الطريقة التي تتواصل بها الودجات الفرعية بالأحداث مع الودجات الأب. فهم وتصميم واجهات الاستدعاء الراجع أمر أساسي للمكونات القابلة لإعادة الاستخدام.
ودجة مخصصة مع استدعاءات راجعة
// ودجة تقييم بالنجوم قابلة لإعادة الاستخدام تتواصل عبر الاستدعاءات الراجعة
class StarRating extends StatelessWidget {
final int rating;
final int maxRating;
final ValueChanged<int> onRatingChanged;
final Color activeColor;
final Color inactiveColor;
final double size;
const StarRating({
super.key,
required this.rating,
this.maxRating = 5,
required this.onRatingChanged,
this.activeColor = Colors.amber,
this.inactiveColor = Colors.grey,
this.size = 32,
});
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(maxRating, (index) {
return GestureDetector(
onTap: () => onRatingChanged(index + 1),
child: Icon(
index < rating ? Icons.star : Icons.star_border,
color: index < rating ? activeColor : inactiveColor,
size: size,
),
);
}),
);
}
}
// الاستخدام
StarRating(
rating: _currentRating,
onRatingChanged: (int newRating) {
setState(() => _currentRating = newRating);
},
)
مثال عملي: حقل نموذج قابل لإعادة الاستخدام
ودجة حقل نموذج مخصصة تغلف التنسيق والتحقق وعرض الأخطاء في مكون قابل لإعادة الاستخدام.
LabeledTextField قابل لإعادة الاستخدام
class LabeledTextField extends StatelessWidget {
final String label;
final String? hint;
final IconData? prefixIcon;
final bool obscureText;
final TextEditingController? controller;
final String? Function(String?)? validator;
final ValueChanged<String>? onChanged;
final TextInputType keyboardType;
const LabeledTextField({
super.key,
required this.label,
this.hint,
this.prefixIcon,
this.obscureText = false,
this.controller,
this.validator,
this.onChanged,
this.keyboardType = TextInputType.text,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: Theme.of(context).textTheme.labelLarge?.copyWith(
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
TextFormField(
controller: controller,
obscureText: obscureText,
keyboardType: keyboardType,
validator: validator,
onChanged: onChanged,
decoration: InputDecoration(
hintText: hint,
prefixIcon: prefixIcon != null ? Icon(prefixIcon) : null,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 14,
),
),
),
],
),
);
}
}
// الاستخدام في نموذج
Form(
key: _formKey,
child: Column(
children: [
LabeledTextField(
label: 'الاسم الكامل',
hint: 'أدخل اسمك الكامل',
prefixIcon: Icons.person,
validator: (value) {
if (value == null || value.isEmpty) {
return 'الاسم مطلوب';
}
return null;
},
),
LabeledTextField(
label: 'البريد الإلكتروني',
hint: 'you@example.com',
prefixIcon: Icons.email,
keyboardType: TextInputType.emailAddress,
validator: (value) {
if (value == null || !value.contains('@')) {
return 'أدخل بريداً إلكترونياً صالحاً';
}
return null;
},
),
LabeledTextField(
label: 'كلمة المرور',
hint: '8 أحرف على الأقل',
prefixIcon: Icons.lock,
obscureText: true,
),
],
),
)
مثال عملي: مكون بطاقة مخصص
بطاقة معلومات قابلة لإعادة الاستخدام تقبل محتوى ديناميكي من خلال التركيب.
ودجة InfoCard
class InfoCard extends StatelessWidget {
final String title;
final String? subtitle;
final IconData icon;
final Color iconColor;
final Widget? trailing;
final VoidCallback? onTap;
const InfoCard({
super.key,
required this.title,
this.subtitle,
required this.icon,
this.iconColor = Colors.blue,
this.trailing,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Card(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: iconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: iconColor),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: Theme.of(context).textTheme.titleMedium
?.copyWith(fontWeight: FontWeight.w600),
),
if (subtitle != null) ...[
const SizedBox(height: 4),
Text(
subtitle!,
style: Theme.of(context).textTheme.bodySmall
?.copyWith(color: Colors.grey),
),
],
],
),
),
if (trailing != null) trailing!,
],
),
),
),
);
}
}
// الاستخدام
InfoCard(
icon: Icons.cloud_download,
iconColor: Colors.green,
title: 'اكتمل التحميل',
subtitle: 'report_2024.pdf (2.3 MB)',
trailing: const Icon(Icons.chevron_right),
onTap: () => debugPrint('فتح الملف'),
)
مثال عملي: ودجة مبنية على البيانات
بناء ودجة تُعرض ديناميكياً بناءً على نموذج بيانات مع الجمع بين البناة والتركيب.
قائمة إعدادات مبنية على البيانات
class SettingItem {
final String title;
final String? subtitle;
final IconData icon;
final Color iconColor;
final Widget Function(BuildContext context) trailingBuilder;
const SettingItem({
required this.title,
this.subtitle,
required this.icon,
this.iconColor = Colors.blue,
required this.trailingBuilder,
});
}
class SettingsList extends StatelessWidget {
final String sectionTitle;
final List<SettingItem> items;
const SettingsList({
super.key,
required this.sectionTitle,
required this.items,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 24, 16, 8),
child: Text(
sectionTitle.toUpperCase(),
style: Theme.of(context).textTheme.labelSmall?.copyWith(
color: Colors.grey,
letterSpacing: 1.2,
),
),
),
Card(
margin: const EdgeInsets.symmetric(horizontal: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: items.asMap().entries.map((entry) {
final index = entry.key;
final item = entry.value;
return Column(
children: [
ListTile(
leading: Container(
padding: const EdgeInsets.all(6),
decoration: BoxDecoration(
color: item.iconColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(item.icon, color: item.iconColor, size: 20),
),
title: Text(item.title),
subtitle: item.subtitle != null
? Text(item.subtitle!)
: null,
trailing: item.trailingBuilder(context),
),
if (index < items.length - 1)
const Divider(height: 1, indent: 56),
],
);
}).toList(),
),
),
],
);
}
}
// الاستخدام
SettingsList(
sectionTitle: 'التفضيلات',
items: [
SettingItem(
title: 'الوضع الداكن',
icon: Icons.dark_mode,
iconColor: Colors.indigo,
trailingBuilder: (ctx) => Switch(
value: _darkMode,
onChanged: (v) => setState(() => _darkMode = v),
),
),
SettingItem(
title: 'اللغة',
subtitle: 'العربية',
icon: Icons.language,
iconColor: Colors.teal,
trailingBuilder: (ctx) => const Icon(Icons.chevron_right),
),
SettingItem(
title: 'حجم الخط',
icon: Icons.text_fields,
iconColor: Colors.orange,
trailingBuilder: (ctx) => DropdownButton<String>(
value: _fontSize,
underline: const SizedBox(),
items: ['صغير', 'متوسط', 'كبير']
.map((s) => DropdownMenuItem(value: s, child: Text(s)))
.toList(),
onChanged: (v) => setState(() => _fontSize = v!),
),
),
],
)
ملخص
- استخراج الودجات -- تقسيم دوال build الضخمة إلى فئات ودجات صغيرة ومركّزة
- LayoutBuilder -- الاستجابة لقيود العنصر الأب للتخطيطات المتجاوبة
- FutureBuilder -- بناء واجهة المستخدم بناءً على نتائج Future غير المتزامنة (تحميل، خطأ، بيانات)
- StreamBuilder -- بناء واجهة المستخدم بشكل تفاعلي من تدفقات البيانات المستمرة
- أنماط الاستدعاء الراجع -- استخدام
onPressedوonChangedوValueChanged<T>لتوصيل الأحداث للأعلى - مكونات قابلة لإعادة الاستخدام -- قبول المعاملات والاستدعاءات الراجعة لجعل الودجات قابلة للتكوين والتركيب
- ودجات مبنية على البيانات -- استخدام النماذج واستدعاءات البناء لعرض محتوى ديناميكي
تمرين عملي
أنشئ ودجة ProductCard قابلة لإعادة الاستخدام تقبل نموذج منتج (الاسم، السعر، رابط الصورة، التقييم، متوفر). أضف ودجة StarRating فرعية وزر "أضف إلى السلة" مع استدعاء راجع onAddToCart وشارة توفر. ثم ابنِ شاشة قائمة منتجات تستخدم FutureBuilder لتحميل قائمة المنتجات وعرضها في شبكة باستخدام LayoutBuilder لعرض عمودين على الهواتف و3 أعمدة على الأجهزة اللوحية.