InheritedNotifier
ما هو InheritedNotifier؟
InheritedNotifier هو صنف فرعي خاص من InheritedWidget يعيد بناء الودجات التابعة تلقائيًا عندما يُطلق Listenable (عادةً ChangeNotifier) إشعارًا. مع InheritedWidget القياسي، يجب عليك تحديد متى يجب تحديث التابعين يدويًا عبر تجاوز updateShouldNotify. يزيل InheritedNotifier هذا العبء تمامًا — فهو يستمع إلى المُبلِّغ الذي تقدمه ويُشغّل إعادة البناء نيابةً عنك.
InheritedWidget يعيد بناء التابعين فقط عندما تُعاد بناء الودجة الأب و تُرجع updateShouldNotify القيمة true. أما InheritedNotifier فيعيد بناء التابعين عندما يستدعي Listenable المرفق notifyListeners()، بغض النظر عن إعادة بناء الأب.
InheritedNotifier مقابل InheritedWidget
دعنا نقارن النهجين جنبًا إلى جنب لفهم لماذا يكون InheritedNotifier غالبًا الخيار الأفضل عندما تكون حالتك مدفوعة بـ ChangeNotifier.
InheritedWidget القياسي (النهج اليدوي)
class CounterInherited extends InheritedWidget {
final int count;
const CounterInherited({
super.key,
required this.count,
required super.child,
});
static CounterInherited of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<CounterInherited>()!;
}
@override
bool updateShouldNotify(CounterInherited oldWidget) {
return count != oldWidget.count;
}
}
مع النهج القياسي يجب عليك إعادة بناء الودجة التي تحتوي على CounterInherited في كل مرة يتغير فيها العداد. عادةً يعني ذلك استدعاء setState في ودجة StatefulWidget أب، مما يُشغّل إعادة بناء كاملة للشجرة الفرعية فوق InheritedWidget.
استخدام ChangeNotifier مع InheritedNotifier
ChangeNotifier هو صنف بسيط يحتفظ بقائمة من المستمعين ويُبلّغهم عند تغيّر الحالة. يتصل InheritedNotifier مباشرةً بـ ChangeNotifier، لذا عند استدعاء notifyListeners() تُعاد بناء التابعين تلقائيًا.
الخطوة 1 — إنشاء ChangeNotifier
class CounterNotifier extends ChangeNotifier {
int _count = 0;
int get count => _count;
void increment() {
_count++;
notifyListeners();
}
void decrement() {
if (_count > 0) {
_count--;
notifyListeners();
}
}
void reset() {
_count = 0;
notifyListeners();
}
}
الخطوة 2 — إنشاء غلاف InheritedNotifier
class CounterProvider extends InheritedNotifier<CounterNotifier> {
const CounterProvider({
super.key,
required CounterNotifier super.notifier,
required super.child,
});
static CounterNotifier of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<CounterProvider>()!
.notifier!;
}
}
of هي اصطلاح يجعل الوصول إلى المُبلِّغ من الودجات الفرعية نظيفًا وموجزًا. احرص دائمًا على تضمين تأكيد عدم القيمة الفارغة (!) أو توفير قيمة بديلة لالتقاط الأخطاء مبكرًا.
إعادة البناء التلقائية عند الإشعار
جمال InheritedNotifier هو أنك لا تحتاج أبدًا لاستدعاء setState في الأب. المُبلِّغ نفسه يقود إعادة البناء. إليك مثالاً كاملاً يربط كل شيء معًا:
مثال العداد الكامل
class CounterApp extends StatefulWidget {
const CounterApp({super.key});
@override
State<CounterApp> createState() => _CounterAppState();
}
class _CounterAppState extends State<CounterApp> {
final _counter = CounterNotifier();
@override
void dispose() {
_counter.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return CounterProvider(
notifier: _counter,
child: const Scaffold(
body: Center(child: CounterDisplay()),
floatingActionButton: CounterButtons(),
),
);
}
}
class CounterDisplay extends StatelessWidget {
const CounterDisplay({super.key});
@override
Widget build(BuildContext context) {
final counter = CounterProvider.of(context);
return Text(
'\${counter.count}',
style: const TextStyle(fontSize: 48),
);
}
}
class CounterButtons extends StatelessWidget {
const CounterButtons({super.key});
@override
Widget build(BuildContext context) {
final counter = CounterProvider.of(context);
return Column(
mainAxisSize: MainAxisSize.min,
children: [
FloatingActionButton(
onPressed: counter.increment,
child: const Icon(Icons.add),
),
const SizedBox(height: 8),
FloatingActionButton(
onPressed: counter.decrement,
child: const Icon(Icons.remove),
),
],
);
}
}
ChangeNotifier في دالة dispose الخاصة بـ StatefulWidget المالكة. عدم القيام بذلك يسبب تسريبات ذاكرة لأن المستمعين يبقون مسجلين حتى بعد إزالة الودجة من الشجرة.
إنشاء أغلفة InheritedNotifier قابلة لإعادة الاستخدام
يمكنك إنشاء غلاف عام قابل لإعادة الاستخدام يعمل مع أي صنف فرعي من ChangeNotifier. هذا يزيل الكود المتكرر عندما يكون لديك عدة مُبلِّغين في تطبيقك.
غلاف InheritedNotifier العام
class NotifierProvider<T extends ChangeNotifier>
extends InheritedNotifier<T> {
const NotifierProvider({
super.key,
required T super.notifier,
required super.child,
});
static T of<T extends ChangeNotifier>(BuildContext context) {
final provider = context
.dependOnInheritedWidgetOfExactType<NotifierProvider<T>>();
assert(provider != null,
'No NotifierProvider<\$T> found in context');
return provider!.notifier!;
}
}
// الاستخدام مع أي مُبلِّغ:
// NotifierProvider<CounterNotifier>(notifier: counter, child: ...)
// final counter = NotifierProvider.of<CounterNotifier>(context);
مثال عملي: مبدّل السمة
حالة استخدام شائعة هي التبديل بين السمة الفاتحة والداكنة. يخزن ThemeNotifier الوضع الحالي ويُبلّغ شجرة الودجات عند تغييره.
مبدّل السمة مع InheritedNotifier
class ThemeNotifier extends ChangeNotifier {
ThemeMode _mode = ThemeMode.light;
ThemeMode get mode => _mode;
bool get isDark => _mode == ThemeMode.dark;
void toggle() {
_mode = _mode == ThemeMode.light
? ThemeMode.dark
: ThemeMode.light;
notifyListeners();
}
void setMode(ThemeMode newMode) {
if (_mode != newMode) {
_mode = newMode;
notifyListeners();
}
}
}
class ThemeProvider extends InheritedNotifier<ThemeNotifier> {
const ThemeProvider({
super.key,
required ThemeNotifier super.notifier,
required super.child,
});
static ThemeNotifier of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<ThemeProvider>()!
.notifier!;
}
}
// في ودجتك الجذرية:
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
final _theme = ThemeNotifier();
@override
void dispose() {
_theme.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ThemeProvider(
notifier: _theme,
child: Builder(
builder: (ctx) {
final theme = ThemeProvider.of(ctx);
return MaterialApp(
themeMode: theme.mode,
theme: ThemeData.light(useMaterial3: true),
darkTheme: ThemeData.dark(useMaterial3: true),
home: const HomeScreen(),
);
},
),
);
}
}
class ThemeToggleButton extends StatelessWidget {
const ThemeToggleButton({super.key});
@override
Widget build(BuildContext context) {
final theme = ThemeProvider.of(context);
return IconButton(
icon: Icon(theme.isDark ? Icons.light_mode : Icons.dark_mode),
onPressed: theme.toggle,
);
}
}
مثال عملي: سلة التسوق
سلة التسوق هي مثال ممتاز من الواقع. تحتاج عدة ودجات لقراءة محتويات السلة — شارة السلة وصفحة السلة وملخص الدفع.
سلة التسوق مع InheritedNotifier
class CartItem {
final String id;
final String name;
final double price;
int quantity;
CartItem({
required this.id,
required this.name,
required this.price,
this.quantity = 1,
});
double get total => price * quantity;
}
class CartNotifier extends ChangeNotifier {
final List<CartItem> _items = [];
List<CartItem> get items => List.unmodifiable(_items);
int get itemCount => _items.fold(0, (sum, i) => sum + i.quantity);
double get totalPrice => _items.fold(0, (sum, i) => sum + i.total);
void addItem(String id, String name, double price) {
final index = _items.indexWhere((i) => i.id == id);
if (index >= 0) {
_items[index].quantity++;
} else {
_items.add(CartItem(id: id, name: name, price: price));
}
notifyListeners();
}
void removeItem(String id) {
_items.removeWhere((i) => i.id == id);
notifyListeners();
}
void clear() {
_items.clear();
notifyListeners();
}
}
class CartProvider extends InheritedNotifier<CartNotifier> {
const CartProvider({
super.key,
required CartNotifier super.notifier,
required super.child,
});
static CartNotifier of(BuildContext context) {
return context
.dependOnInheritedWidgetOfExactType<CartProvider>()!
.notifier!;
}
}
// شارة السلة التي تتحدث تلقائيًا:
class CartBadge extends StatelessWidget {
const CartBadge({super.key});
@override
Widget build(BuildContext context) {
final cart = CartProvider.of(context);
return Badge(
label: Text('\${cart.itemCount}'),
child: const Icon(Icons.shopping_cart),
);
}
}
List.unmodifiable من دالة الحصول على القيم في المُبلِّغ لمنع الكود الخارجي من تعديل القائمة مباشرةً. يجب أن تمر جميع التعديلات عبر دوال المُبلِّغ حتى يتم استدعاء notifyListeners() دائمًا.
الملخص
InheritedNotifierيمتد منInheritedWidgetويستمع تلقائيًا إلىListenable.- يزيل الحاجة إلى
updateShouldNotify— إعادة البناء مدفوعة بـnotifyListeners(). - لا حاجة لـ
setStateفي الودجة الأب؛ المُبلِّغ يقود جميع عمليات إعادة بناء الودجات التابعة. - يمكنك إنشاء أغلفة عامة قابلة لإعادة الاستخدام لأي صنف فرعي من
ChangeNotifier. - احرص دائمًا على التخلص من
ChangeNotifierفي دالةdisposeالخاصة بـStatefulWidgetالمالكة. - تشمل حالات الاستخدام الشائعة العدادات ومبدّلات السمة وسلال التسوق وأي حالة مشتركة قابلة للتغيير.