ما هي الـ 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'),
);

النتيجة

size=Size(120.0, 60.0), pos=Offset(0.0, 0.0)

مثال عملي يوضح فائدة المفاتيح في فلاتر

في الأمثلة التالية، لدينا قائمة من المهام يمكن تعديل عناصرها، و يمكن حذفها.

بشكل عام، قمنا بإنشاء الكلاس 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),
    ),
  );
}

عند تجربة الكود، تستطيع أن ترى أن العنصر المحذوف هو فقط من تنحذف بياناته و لا تنتقل الى عنصر آخر.


في النهاية نستنتج أن استخدام المفاتيح بشكل صحيح يتساعد في تحسين تجربة المستخدم في التطبيق و تحسين الأداء من خلال حفظ الحالة الخاصة بكل عنصر كما أنه يقليل بشكل كبير من عمليات البناء المكلفة.

آخر تحديث في 04-09-2025

Rashed Kamal

مهندس برمجيات و متخصص في مجال تطوير تطبيقات الجوال و اعمل ب Kotlin و Flutter و اقوم بكتابة المحتوى و المقالات التقنية.

Rashedswen.dev

تعليقات

لا يوجد أي تعليق بعد

أضف تعليق

يجب تسجيل الدخول حتى تتمكن من إضافة تعليق أو رد.