ما هي الـ Keys في Flutter و كيف تستخدمها
- مقدمة
- ما هو المفتاح في فلاتر
- أنواع المفاتيح في فلاتر
- مثال عملي يوضح فائدة المفاتيح في فلاتر
مقدمة
تلاحظ في بعض الأحيان أن الودجت ( Widget ) يفقد حالته ( State ) أثناء وجود المستخدم في نفس الشاشة، خصوصاً إذا تغيّر مكانه في شجرة الودجت ( Widget Tree ).
غالباً ما يكون سبب هذه المشكلة هو عدم استخدام المفاتيح ( Keys ) أو أنه تم استخدامها و لكن بشكل خاطئ.
ما هو المفتاح في فلاتر
عندما يُعاد بناء واجهة المستخدم (مثلاً بسبب تغيير الحالة أو الانتقال بين الشاشات) فإن فلاتر قد يعيد إنشاء الودجت التي سبق و أنشأها و يتعامل معها على أنها عنصر جديد و بالتالي فإنه يفقد حالتها السابقة.
لجعل فلاتر لا يعيد إنشاء الودجت، يجب إضافة مفتاح ( Key ) للودجت و هكذا سيعرف أن هذا الودجت هو نفسه الموجود سابقاً (حتى لو تغيّر موقعه في الشجرة) و بالتالي يحتفظ بحالته.
المفتاح في فلاتر عبارة عن معرّف ( Identifier ) يتم إعطاؤه للودجت بهدف الحفاظ على حالته و هذا يسمح لفلاتر أن يقرر ما إذا كان يجب تحديث العنصر الموجود أو استبداله بالكامل.
متى نستخدم المفاتيح
- عند التعامل مع
List
فيها عناصرStateful
. - عند التعامل مع
List
يمكن ترتيب عناصرها، إضافة عناصر فيها أو حذف عناصر منها.
لا تحتاج استخدام مفتاح إذا كنت تتعامل مع ودجت نوعها Stateless
.
أنواع المفاتيح في فلاتر
يوجد 4 أنواع من المفاتيح فلاتر سنتعرف عليها تباعاً.
1- المفتاح من نوع ValueKey
يتم استخدامه لإضافة مفتاح للودجت عن طريق اعطاءه قيمة محددة كمعرف له.
مثال
ListView.builder( itemCount: todos.length, itemBuilder: (context, index) { final todo = todos[index]; return Dismissible( key: ValueKey(todo.id), // كمفتاح id هنا وضعنا قيمة child: ListTile(title: Text(todo.title)), ); }, );
2- المفتاح من نوع ObjectKey
يشبه ValueKey
لكنه يعتمد على المرجع الخاص بالكائن.
مثال
final users = [User(1, 'Omar'), User(2, 'Laila')]; ListView( children: users.map((user) { return ListTile( key: ObjectKey(user), // هنا وضعنا الكائن نفسه كمفتاح title: Text(user.name), ); }).toList(), );
3- المفتاح من نوع UniqueKey
يقوم بإعطاء قيمة فريدة لكل عنصر تحتاج أن لا يتم إعادة إنشاؤه.
يستخدم عندما تريد تمييز كل عنصر عن الآخر حتى لو متشابهين.
مثال
List items = [ widget(key: UniqueKey()), // هنا قمنا بإعطاء مفتاح موحد للودجت widget(key: UniqueKey()), // هنا قمنا بإعطاء مفتاح موحد للودجت ]; @override Widget build(BuildContext context) { return ListView.builder( itemCount: items.length, itemBuilder: (context, index) { final item = items[index]; return item; }, ); }
المفتاح الموحّد يستخدم خارج الدالة build()
حتى لا يتم إعطاء مفتاح جديد مع كل إعادة بناء للواجهة.
4- المفتاح من نوع GlobalKey
يسمح بالوصول إلى حالة الودجت من أي مكان في الشجرة.
يُستخدم في الحالات المتقدمة مثل التحكم في Form
أو Scaffold
.
المثال الأول
final formKey = GlobalKey<FormState>(); // هنا تم تعريف المفتاح Form( key: formKey, // هنا تم تعيين المفتاح للودجت child: TextFormField( validator: (v) => v == null || v.isEmpty ? 'Required' : null, ), ); ElevatedButton( onPressed: () { // هنا تم الوصول لحالة الودجت من خلال المفتاح if (formKey.currentState!.validate()) { // submit } }, child: Text('Submit'), );
و يتيح لك الوصول للودجت و الحصول على بياناتها من أي مكان.
المثال الثاني
final boxKey = GlobalKey(); // هنا تم تعريف المفتاح Container( key: boxKey, // هنا تم تعيين المفتاح للودجت width: 200, height: 100, color: Colors.blue, ); ElevatedButton( onPressed: () { // هنا تم الوصول للودجت من خلال المفتاح final box = boxKey.currentContext!.findRenderObject() as RenderBox; print('Size: ${box.size}'); }, child: Text('Measure'), );
النتيجة
مثال عملي يوضح فائدة المفاتيح في فلاتر
في الأمثلة التالية، لدينا قائمة من المهام يمكن تعديل عناصرها، و يمكن حذفها.
بشكل عام، قمنا بإنشاء الكلاس Todo
يمثل الشكل العامل للبيانات التي سيتم وضعها في كل عنصر في القائمة.
بعدها قمنا بإنشاء القائمة List
و وضعنا فيها ثلاث عناصر، أي ثلاث كائنات من الكلاس Todo
.
في المثال التالي، عناصر القائمة لا تعتمد على مفاتيح.
المثال الأول
class Todo { final String id; final String title; String note; Todo({required this.id, required this.title, this.note = ""}); } final List<Todo> _todos = [ Todo(id: "1", title: "Learn Flutter"), Todo(id: "2", title: "Practice Dart"), Todo(id: "3", title: "Build Apps"), ]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("Without Keys ❌")), body: ListView( children: [ for (int i = 0; i < _todos.length; i++) ListTile( title: TextField( decoration: InputDecoration(labelText: _todos[i].title), ), ), ], ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { if (_todos.isNotEmpty) { _todos.removeAt(0); // نحذف أول عنصر } }); }, child: const Icon(Icons.delete), ), ); }
عند تجربة الكود، فإنه عند حذف العنصر تتحول البيانات الموجودة فيه إلى العنصر التالي في القائمة و بسبب ذلك قد تحصل مشاكل في البيانات إذا أردنا تخزينها، بالإضافة إلى أن المستخدم أيضاً سيرى أن البيانات بها مشكلة و غير متطابقة!
في المثال التالي، أضفنا مفتاح لكل عنصر في القائمة و هذا سيصلح كل الأخطاء السابقة.
المثال الثاني
final List<Todo> _todos = [ Todo(id: "1", title: "Learn Flutter"), Todo(id: "2", title: "Practice Dart"), Todo(id: "3", title: "Build Apps"), ]; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text("With Keys ✅")), body: ListView( children: [ for (int i = 0; i < _todos.length; i++) ListTile( key: ValueKey(_todos[i].id), // ✅ كل عنصر له مفتاح ثابت يميزه عن غيره title: TextField( decoration: InputDecoration(labelText: _todos[i].title), ), ), ], ), floatingActionButton: FloatingActionButton( onPressed: () { setState(() { if (_todos.isNotEmpty) { _todos.removeAt(0); // نحذف أول عنصر } }); }, child: const Icon(Icons.delete), ), ); }
عند تجربة الكود، تستطيع أن ترى أن العنصر المحذوف هو فقط من تنحذف بياناته و لا تنتقل الى عنصر آخر.
في النهاية نستنتج أن استخدام المفاتيح بشكل صحيح يتساعد في تحسين تجربة المستخدم في التطبيق و تحسين الأداء من خلال حفظ الحالة الخاصة بكل عنصر كما أنه يقليل بشكل كبير من عمليات البناء المكلفة.