InheritedWidget هو عنصر واجهة خاص في Flutter ينقل البيانات لأسفل عبر شجرة العناصر دون تمريرها صراحة عبر المنشئات. تصل العناصر الفرعية إلى البيانات من خلال BuildContext وتشترك تلقائياً في التحديثات. عندما تتغير البيانات في InheritedWidget، تُعاد بناء جميع العناصر التابعة. وفقاً لـ Flutter API Reference, 2025، فإن InheritedWidget هو أساس Theme وMediaQuery وLocalizations ومعظم مكتبات إدارة الحالة.
النقاط الرئيسية
InheritedWidget هو عنصر واجهة يجعل بياناته متاحة لجميع العناصر المنحدرة في Widget Tree. على عكس العنصر العادي الذي ينقل البيانات فقط عبر المنشئات إلى العناصر الفرعية، يسمح InheritedWidget لأي عنصر في الشجرة الفرعية بالوصول إلى البيانات دون سلسلة من المعاملات. هذا يحل مشكلة «prop drilling» — تمرير البيانات عبر العديد من العناصر الوسيطة التي لا تستخدم هذه البيانات بنفسها.
يتضمن Flutter العديد من InheritedWidget المضمنة: Theme (نظام الألوان والأنماط)، MediaQuery (حجم الشاشة، الاتجاه، كثافة البكسل)، Localizations (النصوص المترجمة)، Directionality (اتجاه النص)، DefaultTextStyle (نمط النص الافتراضي). هذه العناصر تُعيّن بواسطة العناصر الجذرية مثل MaterialApp وتكون متاحة في جميع أنحاء التطبيق.
InheritedWidget لا يملك حالة خاصة به — بل يخزن البيانات التي تمر عبر المنشئ. عندما يُعاد بناء والد InheritedWidget ببيانات جديدة، يُستدعى method updateShouldNotify لمقارنة البيانات القديمة والجديدة. إذا أعادت الطريقة true، يتم وضع علامة على جميع العناصر التابعة لإعادة البناء. هذه آلية تحديث تفاعلية بسيطة لكنها فعالة.
آلية نقل البيانات عبر InheritedWidget تعتمد على Element Tree. عندما يستدعي عنصر dependOnInheritedWidgetOfExactType، يسجل العنصر المقابل تبعية على InheritedElement. عندما يتغير InheritedWidget، يقوم InheritedElement بإخطار جميع العناصر التابعة، والتي تُعاد بناؤها في الإطار التالي.
طريقة dependOnInheritedWidgetOfExactType لا تجد InheritedWidget في الشجرة فحسب — بل تشترك في الإخطارات للعنصر الحالي. إذا استخدمت findAncestorWidgetOfExactType بدلاً من dependOn، فسيحصل العنصر على البيانات لكنه لن يُعاد بناؤه عندما تتغير. هذا فرق مهم: dependOn هو اشتراك، findAncestor هو بحث لمرة واحدة.
عندما يطلب عنصر InheritedWidget، يصعد Flutter عبر Element Tree من العنصر الحالي إلى الجذر، متحققاً من كل InheritedElement للعثور على تطابق في النوع. يُعاد أول InheritedElement مطابق. هذا يعني أن InheritedWidget الأقرب في الشجرة له الأولوية — يمكنك تجاوز البيانات في مستوى معين بوضع InheritedWidget أقرب إلى المنحدرين.
class ThemeData {
final Color primaryColor;
final TextTheme textTheme;
const ThemeData({required this.primaryColor, required this.textTheme});
}
class MyTheme extends InheritedWidget {
final ThemeData data;
const MyTheme({required this.data, required Widget child}) : super(child: child);
static MyTheme of(BuildContext context) {
final widget = context.dependOnInheritedWidgetOfExactType<MyTheme>();
assert(widget != null, "MyTheme not found in tree");
return widget!;
}
@override
bool updateShouldNotify(MyTheme oldWidget) => oldWidget.data != data;
}
في هذا المثال، MyTheme يستخدم طريقة ثابتة of لتوفير البيانات للمنحدرين. طريقة dependOnInheritedWidgetOfExactType تسجل تبعية، وupdateShouldNotify تقارن البيانات القديمة والجديدة لتحديد ما إذا كانت العناصر التابعة تحتاج إلى إعادة بناء.
إنشاء InheritedWidget مخصص يتكون من خطوتين: تعريف فئة تمد InheritedWidget، وتنفيذ طريقة ثابتة of للوصول من المنحدرين. تُمرر البيانات عبر المنشئ، وتحدد طريقة updateShouldNotify متى يجب إعادة بناء العناصر التابعة.
يجب أن تمد الفئة InheritedWidget وتقبل البيانات عبر منشئ بمعامل child إلزامي. يمكن أن تكون البيانات من أي نوع: أولية، كائنات، دوال. القاعدة الرئيسية هي أن البيانات يجب أن تكون غير قابلة للتغيير بحيث يمكن مقارنة القيم القديمة والجديدة بشكل موثوق.
الطريقة الثابتة of تأخذ BuildContext وتعيد بيانات InheritedWidget. داخلياً، تستدعي dependOnInheritedWidgetOfExactType، التي تجد أقرب InheritedWidget من النوع المحدد في الشجرة. إذا لم يتم العثور على InheritedWidget، تطرح الطريقة استثناءً أو تعيد قيمة افتراضية حسب التنفيذ.
للوصول إلى البيانات، يستدعي العنصر MyWidget.of(context) داخل طريقة البناء. يشترك Flutter تلقائياً في تحديثات العنصر. إذا تغيرت البيانات، يُعاد بناء العنصر في الإطار التالي. هذا يسمح بكتابة كود نظيف وتصريحي بدون معاملات غير ضرورية.
class UserPreferences extends InheritedWidget {
final String languageCode;
final bool darkMode;
const UserPreferences({
required this.languageCode,
required this.darkMode,
required Widget child,
}) : super(child: child);
static UserPreferences of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<UserPreferences>()!;
}
@override
bool updateShouldNotify(UserPreferences oldWidget) =>
oldWidget.languageCode != languageCode || oldWidget.darkMode != darkMode;
}
في هذا المثال، UserPreferences يخزن إعدادات المستخدم. طريقة updateShouldNotify تقارن كل حقل على حدة، مما يمنع عمليات إعادة البناء غير الضرورية عندما يتغير معامل واحد فقط. استخدم نهجاً مشابهاً لـ InheritedWidget الخاصة بك ذات الحقول المتعددة.
updateShouldNotify هي الطريقة الرئيسية لـ InheritedWidget التي تحدد ما إذا كانت العناصر التابعة تحتاج إلى الإخطار بتغييرات البيانات. إذا أعادت الطريقة false، لا تُعاد بناء العناصر التابعة، حتى إذا تلقى InheritedWidget نفسه نسخة جديدة بنفس البيانات. هذا مهم جداً للأداء.
قارن فقط تلك الحقول التي تغيرت فعلاً وتؤثر على العرض. إذا كان InheritedWidget يحتوي على 10 حقول لكن واحداً فقط يؤثر على الواجهة، تحقق من ذلك الحقل فقط. للمجموعات، استخدم المقارنة العميقة أو هياكل البيانات غير القابلة للتغيير. لا تستخدم == مع List أو Map، حيث تتم المقارنة حسب المرجع.
الخطأ الأكثر شيوعاً هو إعادة true بدون مقارنة. هذا يتسبب في إعادة بناء جميع العناصر التابعة عند كل تحديث للوالد، حتى إذا لم تتغير البيانات. الخطأ الثاني هو إعادة false عندما تغيرت البيانات، مما يؤدي إلى واجهة قديمة. الثالث هو المقارنة المعقدة التي تُنفذ كل إطار وتبطئ الأداء.
InheritedWidget والاستدعاءات (تمرير الدوال عبر المنشئات) يحلان مشاكل مختلفة. InheritedWidget مناسب للبيانات التي تحتاجها العديد من العناصر في مستويات مختلفة من الشجرة. الاستدعاءات ملائمة لنقل الأحداث باتجاه واحد من الوالد إلى عنصر فرعي معين أو العكس. يعتمد الاختيار على بنية التطبيق وتكرار التحديثات.
استخدم InheritedWidget عندما تكون البيانات مطلوبة من قبل العديد من العناصر في مستويات تداخل مختلفة: سمة التطبيق، إعدادات المستخدم، معلومات الجهاز، بيانات الجلسة الحالية. InheritedWidget فعال بشكل خاص للبيانات «العالمية» التي نادراً ما تتغير لكنها مطلوبة في أجزاء مختلفة من الواجهة.
الاستدعاءات (دوال رد النداء) مناسبة لنقل الأحداث من عنصر فرعي إلى الوالد: الضغط على زر، اختيار عنصر من قائمة، إرسال نموذج. تشير الاستدعاءات صراحة إلى الإجراءات التي يمكن أن ينفذها العنصر الفرعي، ولا تنشئ تبعيات خفية. لنقل البيانات لأسفل الشجرة عبر عدد صغير من المستويات، من الأسهل أيضاً استخدام معاملات المنشئ.
| المعيار | InheritedWidget | الاستدعاءات |
|---|---|---|
| الاتجاه | من الأعلى للأسفل (الوالد ← المنحدرين) | من الأسفل للأعلى (الفرعي ← الوالد) أو مباشر |
| النطاق | الشجرة الفرعية بأكملها | عنصر محدد |
| إعادة البناء | تلقائية عند تغيير البيانات | تتطلب setState يدوي |
| التعقيد | متوسط (يتطلب فئة InheritedWidget) | منخفض (مجرد دالة) |
Provider وRiverpod هما مكتبتان شائعتان لإدارة الحالة في Flutter مبنيان فوق InheritedWidget. توسعان قدراته: تضيفان دعم ChangeNotifier، الإزالة التلقائية عند الفك، التهيئة البطيئة، وبناء جملة مبسط مع الأنواع العامة.
Provider يستخدم InheritedWidget لتمرير كائن من أي نوع لأسفل الشجرة. ChangeNotifierProvider يتتبع التغييرات عبر ChangeNotifier ويستدعي updateShouldNotify عند استدعاء notifyListeners. هذا يحرر المطور من إنشاء InheritedWidget يدوياً وتنفيذ updateShouldNotify.
InheritedWidget المباشر يعطي تحكماً أكبر ولا يتطلب تبعيات خارجية. Provider يوفر بنية تحتية جاهزة: Consumer، Selector، MultiProvider، ProxyProvider. يعتمد الاختيار على تعقيد التطبيق. للمشاريع البسيطة، InheritedWidget المباشر كافٍ؛ للمشاريع الكبيرة، Provider أو Riverpod يقللان من الكود المتكرر.
// InheritedWidget مباشر
class UserProvider extends InheritedWidget {
final UserData userData;
const UserProvider({required this.userData, required Widget child}) : super(child: child);
static UserData of(BuildContext context) => context.dependOnInheritedWidgetOfExactType<UserProvider>()!.userData;
@override
bool updateShouldNotify(UserProvider old) => old.userData != userData;
}
// مكافئ Provider
return ChangeNotifierProvider<UserData>(
create: (_) => UserData(),
child: MyApp(),
);
كلا النهجين في المثال يحلان نفس المشكلة — تمرير UserData لأسفل الشجرة. Provider يقلل حجم الكود لكنه يخفي آلية InheritedWidget. InheritedWidget المباشر يعطي تحكماً كاملاً وفهماً لما يحدث، وهو مهم بشكل خاص عند تعلم Flutter وتصحيح مشاكل إعادة البناء المعقدة.
الأسئلة الشائعة
InheritedWidget يجعل البيانات متاحة لجميع المنحدرين عبر BuildContext، بينما العنصر العادي يمرر البيانات فقط عبر المنشئ. كما يشترك InheritedWidget للمنحدرين في تحديثات البيانات.
تُعاد بناء العناصر التابعة فقط عندما يُرجع updateShouldNotify القيمة true. إذا نُفذت الطريقة بشكل صحيح، تحدث إعادة البناء فقط عندما تتغير البيانات فعلاً، ليس عند كل إعادة بناء للوالد.
نعم، يمكنك استخدام أي عدد من InheritedWidget في نفس الشجرة. كل واحد يوفر بيانات من نوع محدد، ويمكن للعناصر الحصول على بيانات من عدة InheritedWidget في وقت واحد.
dependOn يشترك في التحديثات — عندما تتغير البيانات، يُعاد بناء العنصر. findAncestor يقوم ببحث لمرة واحدة بدون اشتراك، ولن يعلم العنصر بتغييرات البيانات.
لـ حالة بسيطة (السمة، الإعدادات) InheritedWidget كافٍ. للحالة المعقدة مع منطق الأعمال، استخدم Provider أو Riverpod أو BLoC — فهي مبنية على InheritedWidget وتضيف البنية التحتية اللازمة.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.