ValueNotifier و ValueListenableBuilder
ما هو 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الخاصة بالودجة المالكة. - للكائنات المعقدة، أسند دائمًا نسخة جديدة بدلاً من التعديل في المكان.