InheritedWidget
ما هو InheritedWidget؟
InheritedWidget هو نوع خاص من الودجات في Flutter يسمح بتمرير البيانات بكفاءة عبر شجرة الودجات دون الحاجة لأن يقوم كل ودجت وسيط بتمريرها صراحةً. يحل مشكلة حفر الخصائص (prop drilling) التي ناقشناها في الدرس السابق.
عندما يحتاج ودجت لبيانات من سلف، بدلاً من تلقيها عبر مُنشئه، يمكنه البحث عن أقرب InheritedWidget من نوع معين في الشجرة. هذا البحث هو O(1) — وقت ثابت — لأن Flutter يحتفظ بخريطة لأنواع InheritedWidget ومثيلاتها على كل عنصر.
InheritedWidget هو الأساس الذي بُنيت عليه العديد من حلول إدارة الحالة الشائعة. Provider، على سبيل المثال، هو في الأساس غلاف حول InheritedWidget يضيف ميزات ملائمة. فهم InheritedWidget سيعمق فهمك لكيفية تمرير Flutter للبيانات عبر الشجرة.كيف يوفر InheritedWidget البيانات عبر الشجرة
يجلس InheritedWidget في شجرة الودجات ويجعل بياناته متاحة لجميع الودجات المنحدرة. أي ودجت تحته يمكنه الوصول للبيانات دون حاجة الودجات الوسيطة لمعرفتها.
InheritedWidget مقابل حفر الخصائص
// مع حفر الخصائص (الدرس السابق):
// App -> PageWrapper -> ContentArea -> ProfileSection
// اسم المستخدم يجب تمريره عبر PageWrapper وContentArea
// مع InheritedWidget:
// App
// UserData (InheritedWidget - يحتفظ باسم المستخدم)
// PageWrapper (لا يحتاج معامل اسم المستخدم)
// ContentArea (لا يحتاج معامل اسم المستخدم)
// ProfileSection (يقرأ اسم المستخدم مباشرة من UserData)
// أي ودجت في الشجرة الفرعية يمكنه الوصول للبيانات:
// UserData.of(context).username
إنشاء InheritedWidget مخصص
لإنشاء InheritedWidget خاص بك، تحتاج إلى:
- الوراثة من
InheritedWidget - إضافة بياناتك كحقول
- تنفيذ
updateShouldNotify()للتحكم في متى يُعاد بناء المعتمدين - توفير طريقة ثابتة
of(context)للوصول الملائم
تنفيذ InheritedWidget أساسي
class UserData extends InheritedWidget {
// البيانات التي يوفرها هذا الودجت للمنحدرين
final String username;
final String email;
final bool isLoggedIn;
const UserData({
super.key,
required this.username,
required this.email,
required this.isLoggedIn,
required super.child,
});
// طريقة ثابتة للوصول الملائم
// الودجات تستدعي UserData.of(context) للحصول على البيانات
static UserData of(BuildContext context) {
final result = context.dependOnInheritedWidgetOfExactType<UserData>();
assert(result != null, 'No UserData found in context');
return result!;
}
// اختياري: وصول بدون تسجيل اعتماد (لا يسجل لإعادة البناء)
static UserData? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<UserData>();
}
// يتحكم في متى يجب إعادة بناء الودجات المعتمدة
@override
bool updateShouldNotify(UserData oldWidget) {
return username != oldWidget.username ||
email != oldWidget.email ||
isLoggedIn != oldWidget.isLoggedIn;
}
}
dependOnInheritedWidgetOfExactType
الطريقة context.dependOnInheritedWidgetOfExactType<T>() تقوم بأمرين:
- البحث عن أقرب
InheritedWidgetسلف من النوعT - تسجيل اعتماد بحيث عندما يتغير
InheritedWidget، يتم إعادة بناء هذا الودجت تلقائياً
استخدام dependOnInheritedWidgetOfExactType
class ProfileSection extends StatelessWidget {
const ProfileSection({super.key});
@override
Widget build(BuildContext context) {
// هذا الودجت يعتمد على UserData
// سيُعاد بناؤه عند تغير UserData
final userData = UserData.of(context);
return Column(
children: [
Text('مرحباً، \${userData.username}'),
Text(userData.email),
if (userData.isLoggedIn)
const Text('أنت مسجل الدخول')
else
const Text('يرجى تسجيل الدخول'),
],
);
}
}
// الودجات الوسيطة لا تحتاج لمعرفة UserData
class PageWrapper extends StatelessWidget {
const PageWrapper({super.key});
@override
Widget build(BuildContext context) {
// لا حاجة لمعامل اسم المستخدم!
return const ContentArea();
}
}
class ContentArea extends StatelessWidget {
const ContentArea({super.key});
@override
Widget build(BuildContext context) {
// لا حاجة لمعامل اسم المستخدم!
return const ProfileSection();
}
}
شرح updateShouldNotify
طريقة updateShouldNotify() تحدد ما إذا كان يجب إعادة بناء الودجات المعتمدة على هذا InheritedWidget عندما يُعاد بناؤه ببيانات جديدة. هذه نقطة تحسين حاسمة.
سيناريوهات updateShouldNotify
class AppConfig extends InheritedWidget {
final String apiBaseUrl;
final bool debugMode;
final String appVersion;
const AppConfig({
super.key,
required this.apiBaseUrl,
required this.debugMode,
required this.appVersion,
required super.child,
});
static AppConfig of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<AppConfig>()!;
}
@override
bool updateShouldNotify(AppConfig oldWidget) {
// أبلغ المعتمدين فقط إذا تغيرت البيانات فعلاً
// إذا أُعيد بناء الأب لكن البيانات نفسها، تخطى إعادة بناء المعتمدين
return apiBaseUrl != oldWidget.apiBaseUrl ||
debugMode != oldWidget.debugMode ||
appVersion != oldWidget.appVersion;
}
}
// مثال: إذا أرجع updateShouldNotify قيمة false،
// لن يُعاد بناء المعتمدين حتى لو أُعيد بناء
// InheritedWidget نفسه.
//
// الأب يُعاد بناؤه بنفس البيانات:
// AppConfig(apiBaseUrl: 'https://api.com', ...)
// updateShouldNotify يرجع false
// المعتمدون لا يُعاد بناؤهم (تحسين!)
//
// الأب يُعاد بناؤه ببيانات مختلفة:
// AppConfig(apiBaseUrl: 'https://staging.api.com', ...)
// updateShouldNotify يرجع true
// كل المعتمدين يُعاد بناؤهم
updateShouldNotify() بعناية دائماً. إرجاع true دائماً سيسبب إعادة بناء غير ضرورية. قارن كل حقل قد يستخدمه المعتمدون. للكائنات المعقدة، فكر في استخدام تجاوز عامل == أو حزمة equatable.نمط of(context)
طريقة of(context) الثابتة هي عرف في Flutter للوصول إلى بيانات InheritedWidget. ترى هذا النمط في جميع أنحاء الإطار نفسه:
طرق of() المدمجة في Flutter
// يستخدم Flutter InheritedWidget داخلياً لأشياء كثيرة:
// هذه كلها أمثلة على نمط of(context)
// السمة
final theme = Theme.of(context);
final primaryColor = theme.colorScheme.primary;
// استعلام الوسائط
final screenWidth = MediaQuery.of(context).size.width;
final padding = MediaQuery.of(context).padding;
// المتنقل
Navigator.of(context).push(...);
// السقالة
ScaffoldMessenger.of(context).showSnackBar(...);
// الترجمات
final locale = Localizations.localeOf(context);
// نمط النص الافتراضي
final textStyle = DefaultTextStyle.of(context).style;
// كل هذه تستخدم InheritedWidget تحت الغطاء!
مثال عملي كامل: موفر السمة
سمة مخصصة مع InheritedWidget
// الخطوة 1: تعريف InheritedWidget
class ThemeProvider extends InheritedWidget {
final bool isDarkMode;
final Color primaryColor;
final double fontSize;
const ThemeProvider({
super.key,
required this.isDarkMode,
required this.primaryColor,
required this.fontSize,
required super.child,
});
static ThemeProvider of(BuildContext context) {
final result =
context.dependOnInheritedWidgetOfExactType<ThemeProvider>();
assert(result != null, 'No ThemeProvider found in context');
return result!;
}
@override
bool updateShouldNotify(ThemeProvider oldWidget) {
return isDarkMode != oldWidget.isDarkMode ||
primaryColor != oldWidget.primaryColor ||
fontSize != oldWidget.fontSize;
}
}
// الخطوة 2: إنشاء StatefulWidget لإدارة الحالة
class ThemeWrapper extends StatefulWidget {
final Widget child;
const ThemeWrapper({super.key, required this.child});
@override
State<ThemeWrapper> createState() => ThemeWrapperState();
}
class ThemeWrapperState extends State<ThemeWrapper> {
bool _isDarkMode = false;
Color _primaryColor = Colors.blue;
double _fontSize = 16.0;
void toggleDarkMode() {
setState(() {
_isDarkMode = !_isDarkMode;
});
}
void setPrimaryColor(Color color) {
setState(() {
_primaryColor = color;
});
}
void setFontSize(double size) {
setState(() {
_fontSize = size;
});
}
@override
Widget build(BuildContext context) {
return ThemeProvider(
isDarkMode: _isDarkMode,
primaryColor: _primaryColor,
fontSize: _fontSize,
child: widget.child,
);
}
}
// الخطوة 3: استخدام البيانات في أي مكان في الشجرة الفرعية
class ThemedCard extends StatelessWidget {
const ThemedCard({super.key});
@override
Widget build(BuildContext context) {
final theme = ThemeProvider.of(context);
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: theme.isDarkMode ? Colors.grey[900] : Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: theme.primaryColor),
),
child: Text(
'هذه البطاقة تستخدم InheritedWidget للتنسيق',
style: TextStyle(
fontSize: theme.fontSize,
color: theme.isDarkMode ? Colors.white : Colors.black,
),
),
);
}
}
كيف يحسن Flutter إعادة البناء
نظام InheritedWidget في Flutter محسن بدرجة عالية. إليك كيف تعمل عملية إعادة البناء:
- عند إعادة بناء
InheritedWidgetببيانات جديدة، يتحقق Flutter منupdateShouldNotify() - إذا أرجع
true، يمشي Flutter عبر قائمة المعتمدين المسجلين فقط - كل معتمد يُعلّم كمتسخ وسيُعاد بناؤه في الإطار التالي
- الودجات التي لم تستدعِ
dependOnInheritedWidgetOfExactType()لا تتأثر
عرض إعادة البناء الانتقائية
class CounterProvider extends InheritedWidget {
final int count;
const CounterProvider({
super.key,
required this.count,
required super.child,
});
static CounterProvider of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CounterProvider>()!;
}
@override
bool updateShouldNotify(CounterProvider oldWidget) {
return count != oldWidget.count;
}
}
// هذا الودجت يعتمد على CounterProvider - سيُعاد بناؤه
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context) {
print('CounterDisplay يُعاد بناؤه');
final count = CounterProvider.of(context).count;
return Text('العد: \$count');
}
}
// هذا الودجت لا يعتمد على CounterProvider - لن يُعاد بناؤه
class StaticHeader extends StatelessWidget {
const StaticHeader({super.key});
@override
Widget build(BuildContext context) {
print('StaticHeader يُعاد بناؤه'); // يُطبع مرة واحدة فقط!
return const Text('أنا لا أتغير أبداً');
}
}
// الاستخدام:
class CounterApp extends StatefulWidget {
const CounterApp({super.key});
@override
State<CounterApp> createState() => _CounterAppState();
}
class _CounterAppState extends State<CounterApp> {
int _count = 0;
@override
Widget build(BuildContext context) {
return CounterProvider(
count: _count,
child: Column(
children: [
const StaticHeader(), // لا يُعاد بناؤه
const CounterDisplay(), // يُعاد بناؤه
ElevatedButton(
onPressed: () => setState(() => _count++),
child: const Text('زيادة'),
),
],
),
);
}
}
child مهم للأداء. عندما تمرر ودجت const كابن لـ InheritedWidget، يمكن لـ Flutter تخطي إعادة بناء تلك الشجرة الفرعية. استخرج دائماً شجرة ودجات الابن إلى ودجت const منفصل عندما يكون ممكناً لتجنب إعادة بناء غير ضرورية للشجرة الفرعية بالكامل.مثال عملي: جلسة المستخدم
جلسة المستخدم مع InheritedWidget
class UserSession extends InheritedWidget {
final String? userId;
final String? displayName;
final String? avatarUrl;
final bool isAuthenticated;
const UserSession({
super.key,
this.userId,
this.displayName,
this.avatarUrl,
required this.isAuthenticated,
required super.child,
});
static UserSession of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<UserSession>()!;
}
static UserSession? maybeOf(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<UserSession>();
}
@override
bool updateShouldNotify(UserSession oldWidget) {
return userId != oldWidget.userId ||
displayName != oldWidget.displayName ||
avatarUrl != oldWidget.avatarUrl ||
isAuthenticated != oldWidget.isAuthenticated;
}
}
// أي ودجت يمكنه التحقق من حالة المصادقة بدون حفر الخصائص
class NavBar extends StatelessWidget {
const NavBar({super.key});
@override
Widget build(BuildContext context) {
final session = UserSession.of(context);
return AppBar(
title: const Text('تطبيقي'),
actions: [
if (session.isAuthenticated) ...[
CircleAvatar(
backgroundImage: session.avatarUrl != null
? NetworkImage(session.avatarUrl!)
: null,
child: session.avatarUrl == null
? Text(session.displayName?[0] ?? '?')
: null,
),
] else ...[
TextButton(
onPressed: () {
// الانتقال لتسجيل الدخول
},
child: const Text('تسجيل الدخول'),
),
],
],
);
}
}
class ProfilePage extends StatelessWidget {
const ProfilePage({super.key});
@override
Widget build(BuildContext context) {
final session = UserSession.of(context);
if (!session.isAuthenticated) {
return const Center(child: Text('يرجى تسجيل الدخول لعرض ملفك الشخصي'));
}
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('مرحباً، \${session.displayName}'),
Text('معرف المستخدم: \${session.userId}'),
],
),
);
}
}
مثال عملي: تكوين التطبيق
InheritedWidget لتكوين التطبيق
class AppConfiguration extends InheritedWidget {
final String apiBaseUrl;
final String appVersion;
final bool maintenanceMode;
final Map<String, bool> featureFlags;
const AppConfiguration({
super.key,
required this.apiBaseUrl,
required this.appVersion,
required this.maintenanceMode,
required this.featureFlags,
required super.child,
});
static AppConfiguration of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<AppConfiguration>()!;
}
bool isFeatureEnabled(String feature) {
return featureFlags[feature] ?? false;
}
@override
bool updateShouldNotify(AppConfiguration oldWidget) {
return apiBaseUrl != oldWidget.apiBaseUrl ||
appVersion != oldWidget.appVersion ||
maintenanceMode != oldWidget.maintenanceMode;
}
}
// الاستخدام في التطبيق
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return AppConfiguration(
apiBaseUrl: 'https://api.myapp.com',
appVersion: '1.2.3',
maintenanceMode: false,
featureFlags: {
'dark_mode': true,
'new_checkout': false,
'ai_search': true,
},
child: MaterialApp(
home: const HomePage(),
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
final config = AppConfiguration.of(context);
if (config.maintenanceMode) {
return const Center(
child: Text('التطبيق تحت الصيانة. يرجى المحاولة لاحقاً.'),
);
}
return Scaffold(
appBar: AppBar(
title: Text('تطبيقي v\${config.appVersion}'),
),
body: Column(
children: [
if (config.isFeatureEnabled('ai_search'))
const AiSearchBar(),
if (config.isFeatureEnabled('new_checkout'))
const NewCheckoutButton()
else
const OldCheckoutButton(),
],
),
);
}
}
InheritedWidget هو آلية Flutter المدمجة لتمرير البيانات بكفاءة عبر شجرة الودجات. يحل مشكلة حفر الخصائص بالسماح لأي ودجت منحدر بالوصول للبيانات مباشرة دون حاجة الودجات الوسيطة لتمريرها. طريقة updateShouldNotify() توفر تحكماً دقيقاً في متى يُعاد بناء المعتمدين. بينما يمكنك استخدام InheritedWidget مباشرة، حزم مثل Provider تبسط النمط وتضيف ميزات إضافية مثل التحميل الكسول والتخلص التلقائي.