المفاتيح وهوية الودجات
فهم هوية الودجات في Flutter
في Flutter، كل ودجت في الشجرة لها هوية -- طريقة يستخدمها الإطار لتحديد ما إذا كانت الودجت هي "نفس" الودجت من بناء إلى آخر. افتراضياً، يحدد Flutter الودجات حسب نوعها وموضعها في شجرة الودجات. لكن عندما تتحرك الودجات أو تُعاد ترتيبها أو تتبادل المواضع، تنهار هذه الآلية الافتراضية. هنا يأتي دور المفاتيح.
توفر المفاتيح هوية صريحة للودجات، تخبر Flutter: "هذه الودجت المحددة يجب أن تُطابق مع هذا العنصر المحدد، بغض النظر عن موضعها في الشجرة." فهم المفاتيح ضروري لبناء تطبيقات Flutter صحيحة وعالية الأداء -- خاصة عند العمل مع القوائم والرسوم المتحركة وحالة النماذج.
كيف تعمل عملية المصالحة في Flutter
عندما يعيد Flutter بناء شجرة الودجات، يشغّل خوارزمية المقارنة لتحديد أي العناصر يجب تحديثها أو إنشاؤها أو التخلص منها. فهم هذه العملية يوضح سبب أهمية المفاتيح:
مشكلة المقارنة بدون مفاتيح
// تخيل قائمة من الصناديق الملونة مع حالة داخلية (عدّاد)
class ColorBox extends StatefulWidget {
final Color color;
const ColorBox({required this.color, super.key});
@override
State<ColorBox> createState() => _ColorBoxState();
}
class _ColorBoxState extends State<ColorBox> {
int _counter = 0;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () => setState(() => _counter++),
child: Container(
color: widget.color,
padding: const EdgeInsets.all(16),
child: Text('النقرات: \$_counter',
style: const TextStyle(color: Colors.white)),
),
);
}
}
// الأب الذي يمكنه تبديل الترتيب
class SwapDemo extends StatefulWidget {
const SwapDemo({super.key});
@override
State<SwapDemo> createState() => _SwapDemoState();
}
class _SwapDemoState extends State<SwapDemo> {
bool _swapped = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
// بدون مفاتيح، التبديل يعيد استخدام العناصر حسب الموضع
// الحالة (_counter) تبقى في الموضع وليس مع اللون!
if (!_swapped) ...[
ColorBox(color: Colors.red), // الموضع 0
ColorBox(color: Colors.blue), // الموضع 1
] else ...[
ColorBox(color: Colors.blue), // الموضع 0
ColorBox(color: Colors.red), // الموضع 1
],
ElevatedButton(
onPressed: () => setState(() => _swapped = !_swapped),
child: const Text('تبديل'),
),
],
);
}
}
أنواع المفاتيح
يوفر Flutter عدة أنواع من المفاتيح، كل منها مصمم لحالات استخدام محددة. اختيار نوع المفتاح الصحيح أمر حاسم للسلوك الصحيح.
ValueKey
ValueKey يحدد الودجت باستخدام قيمة واحدة. إنه نوع المفتاح الأكثر استخداماً وهو مثالي عندما يكون لكل عنصر معرّف فريد ومستقر مثل المعرّف أو البريد الإلكتروني أو الاسم.
أمثلة على ValueKey
// استخدام ValueKey مع معرّف فريد
ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return UserTile(
key: ValueKey(user.id), // معرّف فريد مستقر
user: user,
);
},
)
// ValueKey يستخدم == للمقارنة
ValueKey(42) == ValueKey(42) // true
ValueKey('abc') == ValueKey('abc') // true
ValueKey(42) == ValueKey('42') // false (أنواع مختلفة)
// استخدامات شائعة
TextField(key: ValueKey('email-field')),
Dismissible(key: ValueKey(item.id), child: ...),
AnimatedSwitcher(child: Text(key: ValueKey(text), text)),
ObjectKey
ObjectKey يستخدم هوية الكائن (فحص identical()) بدلاً من المساواة (==). هذا مفيد عندما يمكن أن يكون كائنان متساويين بالقيمة لكنهما نسختان مختلفتان يجب التعامل معهما بشكل مختلف.
ObjectKey مقابل ValueKey
class Product {
final String name;
final double price;
Product(this.name, this.price);
@override
bool operator ==(Object other) =>
other is Product && other.name == name && other.price == price;
@override
int get hashCode => Object.hash(name, price);
}
final product1 = Product('Widget', 9.99);
final product2 = Product('Widget', 9.99);
// ValueKey يقارن بالقيمة (==)
ValueKey(product1) == ValueKey(product2); // true -- نفس القيمة
// ObjectKey يقارن بالهوية (identical())
ObjectKey(product1) == ObjectKey(product2); // false -- نسخ مختلفة
// استخدم ObjectKey عندما يكون لديك قيم مكررة لكن نسخ مختلفة
// تحتاج حالتها الخاصة
ListView(
children: cartItems.map((item) =>
CartItemTile(key: ObjectKey(item), item: item)
).toList(),
)
UniqueKey
UniqueKey ينشئ هوية فريدة جديدة في كل مرة يتم إنشاؤه. إنه مفيد عندما تريد إجبار الودجت على أن تُعامل كودجت جديدة تماماً، مع التخلص من أي حالة سابقة.
UniqueKey لفرض إعادة البناء
// إجبار الودجت على إعادة تعيين حالتها بالكامل
class ResetableForm extends StatefulWidget {
const ResetableForm({super.key});
@override
State<ResetableForm> createState() => _ResetableFormState();
}
class _ResetableFormState extends State<ResetableForm> {
Key _formKey = UniqueKey();
void _resetForm() {
setState(() {
// مفتاح فريد جديد = Flutter يعامل هذا كودجت جديدة تماماً
// كل الحالة الداخلية تُتخلص منها وتُعاد إنشاؤها
_formKey = UniqueKey();
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
MyComplexForm(key: _formKey), // الحالة تُعاد عند تغيير المفتاح
ElevatedButton(
onPressed: _resetForm,
child: const Text('إعادة تعيين النموذج'),
),
],
);
}
}
// تحذير: لا تستخدم UniqueKey() مباشرة في build()
// هذا ينشئ مفتاحاً جديداً كل بناء، يدمر الحالة كل إطار!
// خطأ:
Widget build(BuildContext context) {
return MyWidget(key: UniqueKey()); // الحالة تُدمر كل إعادة بناء!
}
UniqueKey هو أداة هدم متحكم بها. خزّنه في متغير واستبدله فقط عندما تريد عمداً تدمير وإعادة إنشاء حالة الودجت.GlobalKey
GlobalKey هو أقوى أنواع المفاتيح -- وأكثرها تكلفة. يوفر الوصول إلى State وBuildContext الخاصين بالودجت من أي مكان في شجرة الودجات، ويسمح للودجت بالتنقل بين أجزاء مختلفة من الشجرة مع الحفاظ على حالتها.
GlobalKey للوصول إلى الحالة
class ParentWidget extends StatefulWidget {
const ParentWidget({super.key});
@override
State<ParentWidget> createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
// إنشاء GlobalKey للوصول إلى حالة الابن
final GlobalKey<_CounterWidgetState> _counterKey = GlobalKey();
void _incrementFromParent() {
// الوصول المباشر لحالة الابن
_counterKey.currentState?.increment();
}
void _getChildInfo() {
// الوصول إلى سياق الابن وصندوق العرض
final context = _counterKey.currentContext;
if (context != null) {
final box = context.findRenderObject() as RenderBox;
final size = box.size;
final position = box.localToGlobal(Offset.zero);
print('حجم الابن: \$size, الموضع: \$position');
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
CounterWidget(key: _counterKey),
ElevatedButton(
onPressed: _incrementFromParent,
child: const Text('زيادة من الأب'),
),
],
);
}
}
class CounterWidget extends StatefulWidget {
const CounterWidget({super.key});
@override
State<CounterWidget> createState() => _CounterWidgetState();
}
class _CounterWidgetState extends State<CounterWidget> {
int _count = 0;
void increment() => setState(() => _count++);
@override
Widget build(BuildContext context) {
return Text('العدد: \$_count');
}
}
PageStorageKey
PageStorageKey هو مفتاح خاص يُستخدم لحفظ مواضع التمرير وحالة أخرى خاصة بالصفحة عبر تبديل التبويبات أو أحداث التنقل.
PageStorageKey للحفاظ على موضع التمرير
// في TabBarView، يتم حفظ واستعادة موضع التمرير لكل تبويب تلقائياً
// باستخدام PageStorageKey
class TabbedListScreen extends StatelessWidget {
const TabbedListScreen({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 3,
child: Scaffold(
appBar: AppBar(
bottom: const TabBar(
tabs: [
Tab(text: 'الأحدث'),
Tab(text: 'الأكثر شعبية'),
Tab(text: 'المفضلة'),
],
),
),
body: TabBarView(
children: [
// كل قائمة تحافظ على موضع التمرير الخاص بها بشكل مستقل
ListView.builder(
key: const PageStorageKey('recent-list'),
itemCount: 100,
itemBuilder: (ctx, i) => ListTile(title: Text('الأحدث \$i')),
),
ListView.builder(
key: const PageStorageKey('popular-list'),
itemCount: 100,
itemBuilder: (ctx, i) => ListTile(title: Text('الأكثر شعبية \$i')),
),
ListView.builder(
key: const PageStorageKey('favorites-list'),
itemCount: 100,
itemBuilder: (ctx, i) => ListTile(title: Text('المفضلة \$i')),
),
],
),
),
);
}
}
متى تستخدم المفاتيح
ليست كل ودجت تحتاج مفتاحاً. إليك السيناريوهات المحددة التي تكون فيها المفاتيح ضرورية:
1. القوائم القابلة لإعادة الترتيب والحذف
المفاتيح في ReorderableListView
class TodoList extends StatefulWidget {
const TodoList({super.key});
@override
State<TodoList> createState() => _TodoListState();
}
class _TodoListState extends State<TodoList> {
final List<Todo> _todos = [
Todo(id: 1, title: 'شراء البقالة'),
Todo(id: 2, title: 'تمشية الكلب'),
Todo(id: 3, title: 'كتابة الكود'),
];
@override
Widget build(BuildContext context) {
return ReorderableListView(
onReorder: (oldIndex, newIndex) {
setState(() {
if (newIndex > oldIndex) newIndex--;
final item = _todos.removeAt(oldIndex);
_todos.insert(newIndex, item);
});
},
children: _todos.map((todo) =>
Dismissible(
// يجب أن يكون له مفتاح -- مطلوب من Dismissible
key: ValueKey(todo.id),
onDismissed: (_) => setState(() => _todos.remove(todo)),
child: ListTile(
title: Text(todo.title),
leading: Checkbox(
value: todo.isDone,
onChanged: (v) => setState(() => todo.isDone = v!),
),
),
),
).toList(),
);
}
}
2. انتقالات AnimatedSwitcher
المفاتيح لانتقالات الرسوم المتحركة
class AnimatedCounter extends StatelessWidget {
final int count;
const AnimatedCounter({required this.count, super.key});
@override
Widget build(BuildContext context) {
return AnimatedSwitcher(
duration: const Duration(milliseconds: 300),
transitionBuilder: (child, animation) =>
ScaleTransition(scale: animation, child: child),
// المفتاح يخبر AnimatedSwitcher متى "تغيّر" الابن
// بدون مفتاح، نفس نوع الودجت = لا رسوم متحركة
child: Text(
'\$count',
key: ValueKey(count), // مفتاح جديد = ابن جديد = تشغيل الرسوم المتحركة
style: Theme.of(context).textTheme.headlineLarge,
),
);
}
}
3. الحفاظ على الحالة عبر تبديل الودجات
إصلاح خطأ التبديل بالمفاتيح
class SwapDemoFixed extends StatefulWidget {
const SwapDemoFixed({super.key});
@override
State<SwapDemoFixed> createState() => _SwapDemoFixedState();
}
class _SwapDemoFixedState extends State<SwapDemoFixed> {
bool _swapped = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
if (!_swapped) ...[
// المفاتيح تضمن أن الحالة تتبع الودجت وليس الموضع
ColorBox(key: const ValueKey('red'), color: Colors.red),
ColorBox(key: const ValueKey('blue'), color: Colors.blue),
] else ...[
ColorBox(key: const ValueKey('blue'), color: Colors.blue),
ColorBox(key: const ValueKey('red'), color: Colors.red),
],
ElevatedButton(
onPressed: () => setState(() => _swapped = !_swapped),
child: const Text('تبديل'),
),
],
);
}
}
انتشار المفاتيح ونطاقها
المفاتيح محددة النطاق بـالأب. يمكن لودجتين في أبوين مختلفين أن يكون لهما نفس قيمة المفتاح بدون تعارض. هذا مهم لفهمه عند تصحيح المشاكل المتعلقة بالمفاتيح.
مثال على نطاق المفاتيح
// هذه المفاتيح لا تتعارض لأنها في أعمدة أب مختلفة
Column(
children: [
Text(key: ValueKey('title'), 'القسم أ'),
Text(key: ValueKey('body'), 'المحتوى أ'),
],
)
Column(
children: [
// نفس قيم المفاتيح، أب مختلف -- لا تعارض
Text(key: ValueKey('title'), 'القسم ب'),
Text(key: ValueKey('body'), 'المحتوى ب'),
],
)
// لكن: المفاتيح المكررة في نفس الأب تسبب أخطاء!
// هذا يرمي خطأ وقت التشغيل:
Column(
children: [
Text(key: ValueKey('same'), 'الأول'),
Text(key: ValueKey('same'), 'الثاني'), // خطأ: مفتاح مكرر
],
)
مثال عملي: قائمة بطاقات قابلة لإعادة الترتيب مع حالة
لنبنِ مثالاً كاملاً يوضح لماذا المفاتيح ضرورية -- قائمة بطاقات قابلة لإعادة الترتيب حيث كل بطاقة لها حالة قابلة للتوسيع الخاصة بها:
مثال كامل لبطاقات قابلة لإعادة الترتيب
class NoteCard extends StatefulWidget {
final String title;
final String content;
const NoteCard({
required this.title,
required this.content,
super.key,
});
@override
State<NoteCard> createState() => _NoteCardState();
}
class _NoteCardState extends State<NoteCard> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Card(
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Column(
children: [
ListTile(
title: Text(widget.title,
style: const TextStyle(fontWeight: FontWeight.bold)),
trailing: IconButton(
icon: Icon(_isExpanded
? Icons.expand_less
: Icons.expand_more),
onPressed: () =>
setState(() => _isExpanded = !_isExpanded),
),
),
if (_isExpanded)
Padding(
padding: const EdgeInsets.all(16),
child: Text(widget.content),
),
],
),
);
}
}
class NoteListScreen extends StatefulWidget {
const NoteListScreen({super.key});
@override
State<NoteListScreen> createState() => _NoteListScreenState();
}
class _NoteListScreenState extends State<NoteListScreen> {
final List<Map<String, String>> _notes = [
{'id': '1', 'title': 'مفاتيح Flutter', 'content': 'المفاتيح تتحكم بالهوية...'},
{'id': '2', 'title': 'إدارة الحالة', 'content': 'اختر بحكمة...'},
{'id': '3', 'title': 'الأداء', 'content': 'حلّل أولاً...'},
];
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('ملاحظاتي')),
body: ReorderableListView(
onReorder: (oldIndex, newIndex) {
setState(() {
if (newIndex > oldIndex) newIndex--;
final item = _notes.removeAt(oldIndex);
_notes.insert(newIndex, item);
});
},
children: _notes.map((note) => NoteCard(
// المفتاح يضمن أن حالة التوسيع تتبع البطاقة أثناء إعادة الترتيب
key: ValueKey(note['id']),
title: note['title']!,
content: note['content']!,
)).toList(),
),
);
}
}
الوصول إلى حالة الودجت باستخدام GlobalKey
بينما يجب استخدام GlobalKey باعتدال، هناك حالات مشروعة حيث يكون الوصول إلى حالة ودجت ابن هو الحل الأنظف -- التحقق من النماذج هو الأكثر شيوعاً:
GlobalKey مع التحقق من النماذج
class RegistrationForm extends StatefulWidget {
const RegistrationForm({super.key});
@override
State<RegistrationForm> createState() => _RegistrationFormState();
}
class _RegistrationFormState extends State<RegistrationForm> {
// GlobalKey لودجت Form
final _formKey = GlobalKey<FormState>();
String _email = '';
String _password = '';
void _submit() {
// الوصول إلى حالة Form لتشغيل التحقق
if (_formKey.currentState!.validate()) {
_formKey.currentState!.save();
// معالجة التسجيل...
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('جاري التسجيل \$_email...')),
);
}
}
@override
Widget build(BuildContext context) {
return Form(
key: _formKey,
child: Column(
children: [
TextFormField(
decoration: const InputDecoration(labelText: 'البريد الإلكتروني'),
validator: (v) =>
v != null && v.contains('@') ? null : 'بريد إلكتروني غير صالح',
onSaved: (v) => _email = v ?? '',
),
TextFormField(
decoration: const InputDecoration(labelText: 'كلمة المرور'),
obscureText: true,
validator: (v) =>
v != null && v.length >= 8 ? null : '8 أحرف كحد أدنى',
onSaved: (v) => _password = v ?? '',
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _submit,
child: const Text('تسجيل'),
),
],
),
);
}
}
الملخص
- ValueKey -- استخدمه عندما يكون للعناصر قيمة فريدة (معرّف، اسم). الخيار الأكثر شيوعاً.
- ObjectKey -- استخدمه عندما تحتاج مقارنة بالهوية وليس بالقيمة.
- UniqueKey -- استخدمه لفرض إعادة تعيين الحالة. خزّنه في متغير ولا تنشئه أبداً في build().
- GlobalKey -- استخدمه باعتدال للوصول إلى الحالة عبر الشجرة (النماذج، السقالات). مكلف.
- PageStorageKey -- استخدمه للحفاظ على مواضع التمرير عبر تبديل التبويبات.
- أضف مفاتيح للقوائم التي تعيد الترتيب أو تضيف/تحذف عناصر أو تحتاج الحفاظ على الحالة.
- المفاتيح محددة النطاق بالأب -- التكرارات داخل نفس الأب تسبب أخطاء.