Kodokon kodokon.com

إدارة الحالة المُنظَّمة: ChangeNotifier وProvider

شارِك حالة التطبيق باستخدام ChangeNotifier وProvider، مع إتقان watch وread وselect لإعادة بناء موجَّهة.

10 دقيقة · 3 أسئلة

افتح هذا الدرس في Kodokon

يكفي setState ما دامت الحالة تحيا وتموت مع شاشة واحدة. لكن بمجرد أن تُشارَك البيانات - سلة، أو جلسة، أو تفضيلات - يصبح تمريرها من بانٍ إلى بانٍ غير قابل للإدارة. يوفّر ChangeNotifier أبسط قابل للرصد في الـ SDK؛ ويحقنه provider في الشجرة، فلا يعيد بناء سوى الودجتات المشترِكة. ومقارنةً بـ Riverpod أو Bloc، يبقى هذا الاقتران الأسهل تدقيقًا: سحر قليل، واعتمادية واحدة، وآلية يمكنك شرحها في جملة واحدة.

BASH
flutter pub add provider

المُبلِّغ المُصمَّم جيدًا يُغلِّف: حقول خاصة، وجوالِب (getters) للقراءة فقط، وتغييرات لا تتم إلا عبر دوال تنتهي بـ notifyListeners(). وهذا العقد هو ما يضمن ألّا يفلت أيّ تغيير في الحالة من الواجهة.

DART
import 'package:flutter/foundation.dart';

class CartModel extends ChangeNotifier {
  final List<String> _items = [];

  List<String> get items => List.unmodifiable(_items);

  int get count => _items.length;

  void add(String item) {
    _items.add(item);
    notifyListeners();
  }

  void remove(String item) {
    _items.remove(item);
    notifyListeners();
  }
}
حالة خاصة، للقراءة فقط من الخارج، مع الإبلاغ.

ثلاث نقاط دخول، وثلاثة استخدامات. يشترِك context.watch<T>() بالودجت: فيعيد بناءه عند كل notifyListeners. ويقرأ context.read<T>() دون اشتراك: فهو أداة ردود النداء مثل onPressed. ويشترِك context.select<T, R>() في إسقاط فقط: فلا يُعاد بناء الودجت إلا إذا تغيّرت تلك القيمة بالتحديد. القاعدة الذهبية: watch وselect في build فقط، وread في المعالِجات - لا العكس أبدًا.

DART
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CounterModel extends ChangeNotifier {
  int _value = 0;

  int get value => _value;

  void increment() {
    _value++;
    notifyListeners();
  }
}

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: const CounterApp(),
    ),
  );
}

class CounterApp extends StatelessWidget {
  const CounterApp({super.key});

  @override
  Widget build(BuildContext context) {
    final value = context.watch<CounterModel>().value;
    return MaterialApp(
      home: Scaffold(
        body: Center(child: Text('Count: $value')),
        floatingActionButton: FloatingActionButton(
          onPressed: () =>
              context.read<CounterModel>().increment(),
          child: const Icon(Icons.add),
        ),
      ),
    );
  }
}
watch في build للقراءة، وread في رد النداء للفعل.
DART
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

class CartModel extends ChangeNotifier {
  final List<String> _items = [];

  int get count => _items.length;
}

class CartBadge extends StatelessWidget {
  const CartBadge({super.key});

  @override
  Widget build(BuildContext context) {
    final count = context.select(
      (CartModel cart) => cart.count,
    );
    return Badge(
      label: Text('$count'),
      child: const Icon(Icons.shopping_cart),
    );
  }
}
لا يُعاد بناء CartBadge إلا عندما يتغيّر count.

اختبار المعرفة

تأكّد من أنك تذكّرت النقاط الأساسية في هذا الدرس.

  1. داخل onPressed، كيف تُطلق cart.add(...) دون إنشاء اشتراك لا لزوم له؟
    • context.watch<CartModel>().add(...)
    • context.read<CartModel>().add(...)
    • context.select((CartModel c) => c.add(...))
  2. ما الذي يُطلقه notifyListeners() بالضبط؟
    • إعادة بناء الودجتات التي تستخدم البيانات المتغيّرة فقط.
    • إعادة بناء كاملة للتطبيق.
    • إعادة بناء كل الودجتات المشترِكة بالمُبلِّغ، بغضّ النظر عن أيّ البيانات تغيّرت.
  3. لماذا نكشف List.unmodifiable(_items) بدلًا من _items مباشرةً؟
    • لتحسين أداء قراءة القائمة.
    • لمنع تغيير خارجي يبدّل الحالة دون notifyListeners، ومن ثمّ دون إعادة بناء.
    • لأن Provider يرفض حقن قوائم قابلة للتغيير.