أساسيات إدارة الحالة

ValueNotifier و ValueListenableBuilder

45 دقيقة الدرس 6 من 14

ما هو ValueNotifier؟

ValueNotifier هو ChangeNotifier خفيف الوزن يحتفظ بقيمة واحدة ويستدعي notifyListeners() تلقائيًا كلما تغيرت تلك القيمة. بدلاً من إنشاء صنف فرعي كامل من ChangeNotifier بدوال مخصصة، يمكنك استخدام ValueNotifier للحالة البسيطة ذات القيمة الواحدة مثل عداد أو تبديل منطقي أو نص بحث.

إنشاء ValueNotifier

// مُبلِّغ عدد صحيح بسيط
final counter = ValueNotifier<int>(0);

// تبديل منطقي
final isVisible = ValueNotifier<bool>(true);

// قيمة نصية
final searchQuery = ValueNotifier<String>('');

// قيمة قابلة للقيمة الفارغة
final selectedId = ValueNotifier<int?>(null);

// تحديث القيمة — يُبلّغ المستمعين تلقائيًا
counter.value = 10;
isVisible.value = false;
searchQuery.value = 'flutter';
مهم: ValueNotifier يُطلق الإشعارات فقط عندما تكون القيمة الجديدة مختلفة عن القيمة الحالية (يتم فحصها عبر عامل !=). تعيين نفس القيمة مرة أخرى لا يُشغّل إعادة بناء.

ودجة ValueListenableBuilder

ValueListenableBuilder هي ودجة تستمع إلى ValueNotifier وتعيد بناء نفسها فقط كلما تغيرت القيمة. هذه هي الميزة الرئيسية: على عكس setState التي تعيد بناء كامل StatefulWidget، توفر ValueListenableBuilder إعادة بناء محددة ودقيقة.

الاستخدام الأساسي لـ ValueListenableBuilder

class CounterPage extends StatefulWidget {
  const CounterPage({super.key});

  @override
  State<CounterPage> createState() => _CounterPageState();
}

class _CounterPageState extends State<CounterPage> {
  final _counter = ValueNotifier<int>(0);

  @override
  void dispose() {
    _counter.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('العداد')),
      body: Center(
        // فقط هذا الباني يُعاد بناؤه عند تغيّر العداد
        child: ValueListenableBuilder<int>(
          valueListenable: _counter,
          builder: (context, value, child) {
            return Text(
              'العدد: \$value',
              style: const TextStyle(fontSize: 32),
            );
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _counter.value++,
        child: const Icon(Icons.add),
      ),
    );
  }
}

إعادة البناء المحددة: فقط الودجة المستمعة تُعاد بناؤها

معامل child في ValueListenableBuilder هو تحسين قوي. الودجات الممررة كـ child تُبنى مرة واحدة وتُعاد استخدامها عبر عمليات إعادة البناء، مما يوفر العمل غير الضروري.

استخدام معامل child للتحسين

ValueListenableBuilder<int>(
  valueListenable: _counter,
  // الطفل يُبنى مرة واحدة ويُمرر إلى الباني
  child: const Text(
    'العدد الحالي:',
    style: TextStyle(fontSize: 16, color: Colors.grey),
  ),
  builder: (context, value, child) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        // child هي ودجة Text المبنية مسبقًا أعلاه
        child!,
        Text(
          '\$value',
          style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold),
        ),
      ],
    );
  },
)
نصيحة: استخدم دائمًا معامل child للأجزاء الثابتة من واجهة المستخدم داخل ValueListenableBuilder. هذا يمنع Flutter من إعادة بناء الودجات التي لا تتغير أبدًا، مما يحسّن الأداء.

عدة ValueNotifiers

في التطبيقات الحقيقية غالبًا ما تحتاج أكثر من جزء واحد من الحالة. يمكنك تداخل عدة ودجات ValueListenableBuilder أو استخدامها جنبًا إلى جنب. كل واحدة تستمع إلى مُبلِّغها المستقل.

عدة ValueNotifiers مستقلة

class FilterPage extends StatefulWidget {
  const FilterPage({super.key});

  @override
  State<FilterPage> createState() => _FilterPageState();
}

class _FilterPageState extends State<FilterPage> {
  final _searchQuery = ValueNotifier<String>('');
  final _showOnlyActive = ValueNotifier<bool>(false);
  final _sortAscending = ValueNotifier<bool>(true);

  @override
  void dispose() {
    _searchQuery.dispose();
    _showOnlyActive.dispose();
    _sortAscending.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // حقل البحث — يحدّث _searchQuery
        TextField(
          onChanged: (val) => _searchQuery.value = val,
          decoration: const InputDecoration(hintText: 'بحث...'),
        ),

        // تبديل فلتر النشط
        ValueListenableBuilder<bool>(
          valueListenable: _showOnlyActive,
          builder: (context, showActive, _) {
            return SwitchListTile(
              title: const Text('إظهار النشطين فقط'),
              value: showActive,
              onChanged: (val) => _showOnlyActive.value = val,
            );
          },
        ),

        // تبديل اتجاه الترتيب
        ValueListenableBuilder<bool>(
          valueListenable: _sortAscending,
          builder: (context, ascending, _) {
            return TextButton.icon(
              icon: Icon(ascending ? Icons.arrow_upward : Icons.arrow_downward),
              label: Text(ascending ? 'تصاعدي' : 'تنازلي'),
              onPressed: () => _sortAscending.value = !ascending,
            );
          },
        ),

        // قائمة النتائج التي تجمع كل الفلاتر
        Expanded(
          child: ValueListenableBuilder<String>(
            valueListenable: _searchQuery,
            builder: (context, query, _) {
              return ValueListenableBuilder<bool>(
                valueListenable: _showOnlyActive,
                builder: (context, activeOnly, _) {
                  return ValueListenableBuilder<bool>(
                    valueListenable: _sortAscending,
                    builder: (context, ascending, _) {
                      return _buildList(query, activeOnly, ascending);
                    },
                  );
                },
              );
            },
          ),
        ),
      ],
    );
  }

  Widget _buildList(String query, bool activeOnly, bool ascending) {
    // منطق الفلترة والترتيب هنا
    return const ListView(); // عنصر نائب
  }
}

أداء ValueNotifier مقابل setState

مع setState، تُعاد تنفيذ دالة build بالكامل. كل ودجة تُرجعها build يُعاد تقييمها (رغم أن مصالحة Flutter تتخطى الأشجار الفرعية غير المتغيرة). مع ValueListenableBuilder، فقط دالة الباني تُعاد تشغيلها.

مقارنة setState مع ValueNotifier

// النهج 1: setState — كامل build() يُعاد تشغيله
class SetStatePage extends StatefulWidget {
  const SetStatePage({super.key});

  @override
  State<SetStatePage> createState() => _SetStatePageState();
}

class _SetStatePageState extends State<SetStatePage> {
  int _count = 0;

  @override
  Widget build(BuildContext context) {
    // كل هذا يُعاد بناؤه عند كل استدعاء setState
    return Scaffold(
      appBar: AppBar(title: const Text('مثال setState')),
      body: Column(
        children: [
          const ExpensiveHeader(), // يُعاد بناؤه بلا داعٍ!
          Text('العدد: \$_count'),
          const ExpensiveFooter(), // يُعاد بناؤه بلا داعٍ!
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => setState(() => _count++),
        child: const Icon(Icons.add),
      ),
    );
  }
}

// النهج 2: ValueNotifier — فقط نص العدد يُعاد بناؤه
class ValueNotifierPage extends StatefulWidget {
  const ValueNotifierPage({super.key});

  @override
  State<ValueNotifierPage> createState() => _ValueNotifierPageState();
}

class _ValueNotifierPageState extends State<ValueNotifierPage> {
  final _count = ValueNotifier<int>(0);

  @override
  void dispose() {
    _count.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // هذه build() تعمل مرة واحدة فقط
    return Scaffold(
      appBar: AppBar(title: const Text('مثال ValueNotifier')),
      body: Column(
        children: [
          const ExpensiveHeader(), // لا يُعاد بناؤه أبدًا
          ValueListenableBuilder<int>(
            valueListenable: _count,
            builder: (context, value, _) {
              return Text('العدد: \$value'); // فقط هذا يُعاد بناؤه
            },
          ),
          const ExpensiveFooter(), // لا يُعاد بناؤه أبدًا
        ],
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => _count.value++,
        child: const Icon(Icons.add),
      ),
    );
  }
}
تحذير: للكائنات المعقدة (القوائم والخرائط والأصناف المخصصة)، يستخدم ValueNotifier عامل != لاكتشاف التغييرات. إذا عدّلت قائمة في مكانها وأعدت إسنادها، فإن المرجع هو نفسه ولن يُطلق أي إشعار. أنشئ دائمًا نسخة جديدة من القائمة: notifier.value = [...notifier.value, newItem].

التخلص من ValueNotifiers

يجب التخلص من كل ValueNotifier عند إزالة الودجة المالكة من الشجرة. هذا يحرر اشتراكات المستمعين الداخلية ويمنع تسريبات الذاكرة.

نمط التخلص الصحيح

class MyWidget extends StatefulWidget {
  const MyWidget({super.key});

  @override
  State<MyWidget> createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  final _name = ValueNotifier<String>('');
  final _age = ValueNotifier<int>(0);
  final _isActive = ValueNotifier<bool>(false);

  @override
  void dispose() {
    _name.dispose();
    _age.dispose();
    _isActive.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    // شجرة الودجات التي تستخدم المُبلِّغين...
    return const Placeholder();
  }
}

مثال عملي: فلتر البحث

فلتر البحث المؤجل هو مناسب تمامًا لـ ValueNotifier. حقل النص يحدّث المُبلِّغ، وقائمة النتائج تُعاد بناؤها فقط عند تغيّر الاستعلام.

فلتر البحث مع ValueNotifier

class SearchPage extends StatefulWidget {
  const SearchPage({super.key});

  @override
  State<SearchPage> createState() => _SearchPageState();
}

class _SearchPageState extends State<SearchPage> {
  final _query = ValueNotifier<String>('');
  final _items = ['Flutter', 'Dart', 'React', 'Swift', 'Kotlin'];

  @override
  void dispose() {
    _query.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: TextField(
          onChanged: (val) => _query.value = val.toLowerCase(),
          decoration: const InputDecoration(
            hintText: 'بحث...',
            border: InputBorder.none,
          ),
        ),
      ),
      body: ValueListenableBuilder<String>(
        valueListenable: _query,
        builder: (context, query, _) {
          final filtered = _items
              .where((item) => item.toLowerCase().contains(query))
              .toList();
          return ListView.builder(
            itemCount: filtered.length,
            itemBuilder: (context, index) {
              return ListTile(title: Text(filtered[index]));
            },
          );
        },
      ),
    );
  }
}

مثال عملي: تتبع حقول النموذج

تتبع ما إذا كان النموذج صالحًا في الوقت الفعلي من خلال دمج عدة نسخ من ValueNotifier.

التحقق من النموذج مع ValueNotifiers

class SignUpForm extends StatefulWidget {
  const SignUpForm({super.key});

  @override
  State<SignUpForm> createState() => _SignUpFormState();
}

class _SignUpFormState extends State<SignUpForm> {
  final _email = ValueNotifier<String>('');
  final _password = ValueNotifier<String>('');
  final _isFormValid = ValueNotifier<bool>(false);

  @override
  void initState() {
    super.initState();
    // الاستماع لكلا الحقلين وتحديث صلاحية النموذج
    _email.addListener(_validateForm);
    _password.addListener(_validateForm);
  }

  void _validateForm() {
    _isFormValid.value =
        _email.value.contains('@') && _password.value.length >= 8;
  }

  @override
  void dispose() {
    _email.removeListener(_validateForm);
    _password.removeListener(_validateForm);
    _email.dispose();
    _password.dispose();
    _isFormValid.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        TextField(
          onChanged: (val) => _email.value = val,
          decoration: const InputDecoration(labelText: 'البريد الإلكتروني'),
        ),
        TextField(
          onChanged: (val) => _password.value = val,
          obscureText: true,
          decoration: const InputDecoration(labelText: 'كلمة المرور'),
        ),
        const SizedBox(height: 16),
        ValueListenableBuilder<bool>(
          valueListenable: _isFormValid,
          builder: (context, isValid, _) {
            return ElevatedButton(
              onPressed: isValid ? () => _submit() : null,
              child: const Text('تسجيل'),
            );
          },
        ),
      ],
    );
  }

  void _submit() {
    // معالجة إرسال النموذج
  }
}

الملخص

  • ValueNotifier يحتفظ بقيمة واحدة ويُبلّغ المستمعين تلقائيًا عند تغيّرها.
  • ValueListenableBuilder يعيد بناء الودجات داخل بانيه فقط، مما يوفر إعادة بناء محددة.
  • معامل child يحافظ على الودجات الثابتة عبر عمليات إعادة البناء لتحسين الأداء.
  • يمكن استخدام عدة نسخ من ValueNotifier بشكل مستقل أو متداخل.
  • ValueNotifier أكثر أداءً من setState لعزل إعادة البناء لأجزاء محددة من واجهة المستخدم.
  • احرص دائمًا على التخلص من كل ValueNotifier في دالة dispose الخاصة بالودجة المالكة.
  • للكائنات المعقدة، أسند دائمًا نسخة جديدة بدلاً من التعديل في المكان.