لماذا تهم إدارة الحالة على نطاق واسع
لماذا تهم إدارة الحالة على نطاق واسع
عندما تتعلم Flutter للمرة الأولى، يبدو setState() طبيعياً وكافياً. تستدعيه، يُعاد بناء الودجت، تتحدث الشاشة. لكن مع نمو تطبيقك من حفنة شاشات إلى عشرات، ومن مطور واحد إلى فريق، تصبح قيود setState() وحتى InheritedWidget واضحة بشكل مؤلم. فهم لماذا توجد حلول إدارة الحالة المخصصة هو الخطوة الأولى نحو اختيارها واستخدامها بفعالية.
قيود setState
يعمل setState() بشكل جيد مع الحالة المحلية المؤقتة — مربع اختيار، تقدم رسوم متحركة، حالة فتح/إغلاق قائمة منسدلة. تظهر مشاكله فور احتياج الحالة إلى المشاركة أو نمو شجرة الودجات في العمق.
- حفر الخصائص (Prop Drilling): لتمرير البيانات من ودجت عالي المستوى إلى ودجت متداخل عميقاً، يجب تمريرها عبر كل مُنشئ وسيط. تغيير شكل هذه البيانات يفرض تحديثات على كل طبقة.
- إعادة بناء مفرطة: استدعاء
setState()يُعلّم الودجت بالكامل (وجميع أحفاده) كـ"متسخ". في شجرة فرعية كبيرة، يسبب ذلك إعادات بناء مفرطة وغير ضرورية تضر بالأداء. - منطق الأعمال داخل الودجات: عندما تحتوي طريقة
build()أيضاً على استدعاءات غير متزامنة ومنطق تحويل وآثار جانبية، يصبح الودجت غير قابل للاختبار بدون تسخير اختبار ودجت كامل. - غياب فصل الاهتمامات: عرض واجهة المستخدم وجلب البيانات يعيشان في نفس الفئة، مما يجعل كليهما أصعب في الفهم وأصعب في الاختبار باستقلالية.
مشكلة حفر الخصائص (Prop Drilling)
// ودجت متداخل عميقاً يحتاج كائن المستخدم.
// مع setState + تمرير المُنشئ، كل طبقة يجب أن تقبله:
class RootApp extends StatefulWidget {
@override
State<RootApp> createState() => _RootAppState();
}
class _RootAppState extends State<RootApp> {
User? _currentUser;
@override
Widget build(BuildContext context) {
// يجب تمرير _currentUser كل الطريق للأسفل...
return HomeScreen(currentUser: _currentUser);
}
}
class HomeScreen extends StatelessWidget {
final User? currentUser; // يستقبله فقط ليمرره
const HomeScreen({super.key, this.currentUser});
@override
Widget build(BuildContext context) {
return ProfileSection(currentUser: currentUser); // يمرره...
}
}
class ProfileSection extends StatelessWidget {
final User? currentUser; // مجدداً، مجرد نقل
const ProfileSection({super.key, this.currentUser});
@override
Widget build(BuildContext context) {
return UserAvatar(user: currentUser); // يُستخدم هنا أخيراً
}
}
// HomeScreen وProfileSection لا حاجة حقيقية لهما بـ currentUser
// — إنهما فقط ينقلانه. هذا هو حفر الخصائص.
قيود InheritedWidget
كان InheritedWidget الإجابة الأصلية لـ Flutter على مشكلة حفر الخصائص. يضع البيانات في مستوى عالٍ من شجرة الودجات ويسمح للأحفاد بالوصول إليها دون تمرير صريح. لكنه يأتي بعيوبه الجسيمة الخاصة على نطاق واسع:
- كثرة الكود المعياري: كل
InheritedWidgetيتطلب فئة مخصصة، وصول عبرof(context)، وتجاوزupdateShouldNotify()— فقط لإتاحة قطعة بيانات واحدة. - دقة إعادة البناء خشنة: بشكل افتراضي، أي ودجت يستدعي
context.dependOnInheritedWidgetOfExactType()سيُعاد بناؤه كلما تغير أي شيء في الودجت الموروث، حتى لو لم تتغير القيمة التي يهتم بها تحديداً. - بيانات غير قابلة للتغيير فقط: يحمل
InheritedWidgetنفسه بيانات غير قابلة للتغيير. لتحديثها، يجب تغليفه فيStatefulWidgetوتوفير نسخة جديدة — مما يؤدي سريعاً إلى تداخل معقد. - لا دعم لدورة الحياة أو الاستدعاءات غير المتزامنة: لا يوفر آلية مدمجة لتحميل البيانات أو معالجة الأخطاء أو التفاعل مع أحداث التدفق.
عبء الكود المعياري لـ InheritedWidget
// فقط لإتاحة كائن User واحد تحتاج كل هذا:
class UserProvider extends InheritedWidget {
final User? user;
const UserProvider({
super.key,
required this.user,
required super.child,
});
// كل المعتمدين يُعاد بناؤهم كلما عاد updateShouldNotify بـ true
@override
bool updateShouldNotify(UserProvider oldWidget) {
return oldWidget.user != user;
}
// الوصول الذي يجب على الأحفاد استخدامه
static UserProvider? of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<UserProvider>();
}
}
// ولجعله قابلاً للتغيير، تحتاج أيضاً إلى StatefulWidget يغلفه.
// اضرب هذا النمط في كل قطعة من الحالة المشتركة في تطبيقك.
ما يجب أن يوفره حل إدارة الحالة المخصص
مع توسع التطبيقات، يجب على طبقة إدارة الحالة تلبية أربعة متطلبات أساسية:
- القابلية للتنبؤ: بالنظر إلى مجموعة من الإجراءات أو الأحداث، يجب أن تكون الحالة الناتجة حتمية. نفس المدخلات تنتج دائماً نفس المخرجات — لا آثار جانبية خفية، لا ظروف سباق بين الودجات التي تستدعي
setState()بشكل متزامن. - قابلية الاختبار: يجب أن يكون منطق الأعمال قابلاً للاختبار باختبارات وحدة Dart العادية، منفصلاً كلياً عن شجرة الودجات. يجب أن تتمكن من التحقق من انتقالات الحالة دون عرض ودجت واحد.
- فصل الاهتمامات: يجب فصل كود واجهة المستخدم (ماذا تعرض) بشكل نظيف عن منطق الأعمال (كيف تحسبه) والوصول إلى البيانات (من أين تأتي). لكل طبقة مسؤولية واحدة محددة جيداً.
- إعادة بناء فعّالة: يجب أن تُعاد الودجات التي تعتمد على قطعة محددة من الحالة فقط عند تغير تلك الحالة. تبقى بقية الشجرة دون تغيير.
setState() وInheritedWidget ليسا خاطئين — إنهما الأساس الذي تبني عليه كل الحلول المتقدمة. يستخدم Bloc وRiverpod وProvider كلاهما InheritedWidget داخلياً. ما يضيفونه هو البنية والاتفاقيات والأدوات التي تجعل قواعد الكود الكبيرة قابلة للصيانة.سيناريو ملموس: نقطة الانهيار
تخيل تطبيق محادثة يحتوي على قائمة رسائل، وشارة عدد غير المقروءة في شريط التنقل السفلي، ونقطة إشعار على أيقونة الملف الشخصي، وصفحة إعدادات تتحكم في تفضيلات الإشعارات. جميع الودجات الأربعة تتفاعل مع نفس بيانات "الرسائل" الأساسية. باستخدام setState() وحده، ستحتاج إلى:
- رفع الحالة كل الطريق إلى الودجت الجذر
- تمرير ردود النداء والبيانات عبر كل ودجت وسيط
- تشغيل إعادة بناء شجرة فرعية كاملة مع كل رسالة جديدة
- تكرار منطق الاستطلاع/التدفق في أماكن متعددة
يضع الحل المخصص تدفق الرسائل في كائن مركزي واحد. كل ودجت يشترك بشكل مستقل ويُعيد بناء نفسه فقط عند تغير الشريحة المعنية من البيانات.
build() مئة سطر. البدء بـ setState() والترحيل التدريجي استراتيجية مشروعة.ملخص
setState() أداة قوية للحالة المحلية لكنها تنهار في ثلاثة أحوال: الحالة المشتركة بعمق، أشجار الودجات الكبيرة التي تتطلب إعادات بناء جزئية فعّالة، وقواعد الكود التي تحتاج منطق أعمال مُختبَر بالوحدة. يحل InheritedWidget مشكلة المشاركة لكنه منخفض المستوى ومفرط التفصيل للتطبيقات الحقيقية. يجب أن يوفر حل إدارة الحالة الجاهز للإنتاج — مثل Bloc أو Riverpod — القابلية للتنبؤ (انتقالات حتمية)، وقابلية الاختبار (منطق مفصول عن الودجات)، وفصل الاهتمامات (واجهة المستخدم مقابل المنطق مقابل البيانات)، وإعادات البناء الفعّالة (تحديثات الودجات الجراحية). هذه الركائز الأربع هي العدسة التي سيُقيَّم من خلالها كل حل في هذا البرنامج التعليمي.