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

رفع الحالة لأعلى

50 دقيقة الدرس 3 من 14

لماذا نرفع الحالة لأعلى؟

في Flutter، تتدفق البيانات نحو الأسفل عبر شجرة الودجات من خلال معاملات المُنشئ. عندما يحتاج ودجتان أو أكثر من الأشقاء لمشاركة نفس جزء من الحالة، لا يمكن لأي ودجت امتلاك تلك الحالة وحده. الحل هو رفع الحالة لأعلى إلى أقرب سلف مشترك، والذي يمرر بعد ذلك الحالة للأسفل ويوفر ردود استدعاء للأبناء لطلب التغييرات.

هذا النمط أساسي في تطوير Flutter ونشأ من مفهوم React “رفع الحالة لأعلى”. يضمن مصدراً واحداً للحقيقة للبيانات المشتركة ويحافظ على تدفق بيانات أحادي الاتجاه.

المشكلة: الأشقاء لا يمكنهم مشاركة الحالة

// المشكلة: WidgetA وWidgetB كلاهما يحتاج لعرض نفس العداد
// لكنهما أشقاء - لا يمكن لأي منهما الوصول لحالة الآخر

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

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        WidgetA(), // لديه عداده الخاص
        WidgetB(), // يحتاج نفس قيمة العداد
        // كيف نجعلهما متزامنين؟
      ],
    );
  }
}

تواصل الأب والابن عبر ردود الاستدعاء

الآلية الأساسية لرفع الحالة تتضمن اتجاهين للتواصل:

  • من الأب إلى الابن: تمرير البيانات للأسفل كمعاملات مُنشئ
  • من الابن إلى الأب: تمرير دوال رد استدعاء للأسفل يستدعيها الابن لطلب تغييرات الحالة

نمط الحالة المرفوعة الأساسي

// الحل: الأب يمتلك الحالة ويمررها للأسفل

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

  @override
  State<CounterParent> createState() => _CounterParentState();
}

class _CounterParentState extends State<CounterParent> {
  int _counter = 0;

  void _increment() {
    setState(() {
      _counter++;
    });
  }

  void _decrement() {
    setState(() {
      _counter--;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // تمرير الحالة للأسفل كمعاملات
        CounterDisplay(count: _counter),
        // تمرير ردود الاستدعاء للأسفل ليطلب الابن التغييرات
        CounterControls(
          onIncrement: _increment,
          onDecrement: _decrement,
        ),
      ],
    );
  }
}

// هذا الودجت يعرض العداد فقط - بدون حالة
class CounterDisplay extends StatelessWidget {
  final int count;
  const CounterDisplay({super.key, required this.count});

  @override
  Widget build(BuildContext context) {
    return Text(
      '\$count',
      style: const TextStyle(fontSize: 48),
    );
  }
}

// هذا الودجت يوفر عناصر التحكم فقط - بدون حالة
class CounterControls extends StatelessWidget {
  final VoidCallback onIncrement;
  final VoidCallback onDecrement;

  const CounterControls({
    super.key,
    required this.onIncrement,
    required this.onDecrement,
  });

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        IconButton(
          onPressed: onDecrement,
          icon: const Icon(Icons.remove_circle),
        ),
        IconButton(
          onPressed: onIncrement,
          icon: const Icon(Icons.add_circle),
        ),
      ],
    );
  }
}

تمرير الحالة للأسفل كمعاملات المُنشئ

عند رفع الحالة لأعلى، كل ابن يحتاج البيانات يتلقاها عبر مُنشئه. هذا يجعل تدفق البيانات صريحاً وسهل التتبع. تصبح ودجات الأبناء عديمة الحالة — دوال نقية لمدخلاتها.

تمرير الحالة عبر مستويات متعددة

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

  @override
  State<ShoppingApp> createState() => _ShoppingAppState();
}

class _ShoppingAppState extends State<ShoppingApp> {
  final List<Product> _cart = [];

  void _addToCart(Product product) {
    setState(() {
      _cart.add(product);
    });
  }

  void _removeFromCart(Product product) {
    setState(() {
      _cart.remove(product);
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // ملخص السلة يتلقى بيانات السلة
        CartSummary(
          itemCount: _cart.length,
          total: _cart.fold(0.0, (sum, p) => sum + p.price),
        ),
        // قائمة المنتجات تتلقى رد استدعاء لإضافة العناصر
        ProductList(
          onAddToCart: _addToCart,
          cartItems: _cart,
        ),
      ],
    );
  }
}

class CartSummary extends StatelessWidget {
  final int itemCount;
  final double total;

  const CartSummary({
    super.key,
    required this.itemCount,
    required this.total,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: const EdgeInsets.all(16),
      color: Colors.blue.shade50,
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text('العناصر: \$itemCount'),
          Text('الإجمالي: \\$\${total.toStringAsFixed(2)}'),
        ],
      ),
    );
  }
}

نمط رد الاستدعاء

يستخدم Flutter ردود الاستدعاء على نطاق واسع للتواصل من الابن إلى الأب. الأنماط الأكثر شيوعاً هي onChanged وonSubmitted وonPressed وردود الاستدعاء المخصصة باستخدام أنواع ValueChanged<T> أو Function.

أنماط رد الاستدعاء الشائعة

// ودجت بحث مخصص مع ردود استدعاء
class SearchBar extends StatelessWidget {
  final String query;
  final ValueChanged<String> onQueryChanged;
  final VoidCallback onClear;
  final ValueChanged<String> onSubmitted;

  const SearchBar({
    super.key,
    required this.query,
    required this.onQueryChanged,
    required this.onClear,
    required this.onSubmitted,
  });

  @override
  Widget build(BuildContext context) {
    return TextField(
      onChanged: onQueryChanged,
      onSubmitted: onSubmitted,
      decoration: InputDecoration(
        hintText: 'بحث...',
        prefixIcon: const Icon(Icons.search),
        suffixIcon: query.isNotEmpty
            ? IconButton(
                onPressed: onClear,
                icon: const Icon(Icons.clear),
              )
            : null,
      ),
    );
  }
}

// الأب يدير حالة البحث
class SearchPage extends StatefulWidget {
  const SearchPage({super.key});

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

class _SearchPageState extends State<SearchPage> {
  String _query = '';
  List<String> _allItems = ['تفاح', 'موز', 'كرز', 'تمر', 'توت بري'];

  List<String> get _filteredItems {
    if (_query.isEmpty) return _allItems;
    return _allItems
        .where((item) => item.toLowerCase().contains(_query.toLowerCase()))
        .toList();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        SearchBar(
          query: _query,
          onQueryChanged: (value) {
            setState(() {
              _query = value;
            });
          },
          onClear: () {
            setState(() {
              _query = '';
            });
          },
          onSubmitted: (value) {
            // تنفيذ إجراء البحث
          },
        ),
        Expanded(
          child: ListView.builder(
            itemCount: _filteredItems.length,
            itemBuilder: (context, index) {
              return ListTile(title: Text(_filteredItems[index]));
            },
          ),
        ),
      ],
    );
  }
}
نصيحة: يوفر Flutter أسماء مستعارة للأنواع لتوقيعات ردود الاستدعاء الشائعة. VoidCallback هو دالة بدون معاملات وبدون قيمة إرجاع. ValueChanged<T> يأخذ معاملاً واحداً من النوع T ويرجع void. استخدم هذه الأنواع القياسية كلما أمكن للتناسق.

مشكلة تمرير الخصائص عبر المستويات (Prop Drilling)

مع نمو شجرة الودجات بشكل أعمق، قد تجد نفسك تمرر الحالة وردود الاستدعاء عبر ودجات وسيطة كثيرة لا تستخدمها. هذا يُعرف بـ حفر الخصائص (prop drilling)، وهو علامة على أنك قد تحتاج نهجاً أكثر تطوراً لإدارة الحالة.

مثال حفر الخصائص

// اسم المستخدم يحتاج للانتقال من App وصولاً إلى DeepChild
// كل ودجت في المنتصف يجب أن يمرره، حتى لو لم يستخدمه

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

  @override
  State<App> createState() => _AppState();
}

class _AppState extends State<App> {
  String _username = 'إدريس';

  @override
  Widget build(BuildContext context) {
    return PageWrapper(
      username: _username,                    // المستوى 1: تمرير للأسفل
      onUsernameChanged: (name) {
        setState(() { _username = name; });
      },
    );
  }
}

class PageWrapper extends StatelessWidget {
  final String username;                      // يجب التصريح
  final ValueChanged<String> onUsernameChanged;

  const PageWrapper({
    super.key,
    required this.username,
    required this.onUsernameChanged,
  });

  @override
  Widget build(BuildContext context) {
    return ContentArea(
      username: username,                     // المستوى 2: تمرير عبر
      onUsernameChanged: onUsernameChanged,
    );
  }
}

class ContentArea extends StatelessWidget {
  final String username;                      // يجب التصريح مرة أخرى
  final ValueChanged<String> onUsernameChanged;

  const ContentArea({
    super.key,
    required this.username,
    required this.onUsernameChanged,
  });

  @override
  Widget build(BuildContext context) {
    return ProfileSection(
      username: username,                     // المستوى 3: تمرير عبر
      onUsernameChanged: onUsernameChanged,
    );
  }
}

class ProfileSection extends StatelessWidget {
  final String username;                      // أخيراً يُستخدم هنا!
  final ValueChanged<String> onUsernameChanged;

  const ProfileSection({
    super.key,
    required this.username,
    required this.onUsernameChanged,
  });

  @override
  Widget build(BuildContext context) {
    return Text('مرحباً، \$username');
  }
}
تحذير: حفر الخصائص ليس خاطئاً بطبيعته، لكن عندما تمرر البيانات عبر 3 مستويات أو أكثر من الودجات التي لا تستخدمها، يصبح عبء صيانة. في كل مرة تضيف جزءاً جديداً من الحالة المشتركة، يجب تعديل كل ودجت وسيط. هذا هو الوقت الذي يجب فيه التفكير في InheritedWidget أو Provider أو حل آخر لإدارة الحالة.

متى يكون رفع الحالة كافياً

رفع الحالة لأعلى هو الخيار الصحيح عندما:

  • فقط 2-3 ودجات تحتاج لمشاركة الحالة
  • الودجات قريبة من بعضها في الشجرة (1-2 مستوى فارق)
  • الحالة المشتركة بسيطة (بضعة متغيرات)
  • سلسلة ردود الاستدعاء قصيرة وواضحة

يجب التفكير في حلول أكثر تقدماً عندما:

  • ودجات كثيرة عبر أشجار فرعية مختلفة تحتاج نفس الحالة
  • تمرر البيانات عبر 3 مستويات أو أكثر من ودجات لا تستخدمها
  • تغييرات الحالة معقدة مع ترابطات كثيرة
  • تحتاج لمشاركة الحالة عبر المسارات أو الشاشات

مثال عملي: قائمة قابلة للتصفية

قائمة قابلة للتصفية مع حالة مرفوعة

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

  @override
  State<FilterableList> createState() => _FilterableListState();
}

class _FilterableListState extends State<FilterableList> {
  String _selectedCategory = 'الكل';
  String _searchQuery = '';

  final List<Map<String, String>> _items = [
    {'name': 'Flutter', 'category': 'جوال'},
    {'name': 'React', 'category': 'ويب'},
    {'name': 'SwiftUI', 'category': 'جوال'},
    {'name': 'Angular', 'category': 'ويب'},
    {'name': 'Kotlin', 'category': 'جوال'},
    {'name': 'Vue.js', 'category': 'ويب'},
  ];

  List<Map<String, String>> get _filteredItems {
    return _items.where((item) {
      final matchesCategory = _selectedCategory == 'الكل' ||
          item['category'] == _selectedCategory;
      final matchesSearch = _searchQuery.isEmpty ||
          item['name']!.toLowerCase().contains(
            _searchQuery.toLowerCase(),
          );
      return matchesCategory && matchesSearch;
    }).toList();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        // شريط البحث - الابن يتواصل للأعلى عبر رد الاستدعاء
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: TextField(
            onChanged: (value) {
              setState(() {
                _searchQuery = value;
              });
            },
            decoration: const InputDecoration(
              hintText: 'بحث في الأُطر...',
              prefixIcon: Icon(Icons.search),
            ),
          ),
        ),
        // تصفية الفئة - الابن يتواصل للأعلى عبر رد الاستدعاء
        CategoryFilter(
          categories: const ['الكل', 'جوال', 'ويب'],
          selected: _selectedCategory,
          onSelected: (category) {
            setState(() {
              _selectedCategory = category;
            });
          },
        ),
        // عدد النتائج - يتلقى الحالة من الأب
        Padding(
          padding: const EdgeInsets.all(8.0),
          child: Text('عرض \${_filteredItems.length} نتيجة'),
        ),
        // قائمة العناصر - تتلقى البيانات المصفاة من الأب
        Expanded(
          child: ListView.builder(
            itemCount: _filteredItems.length,
            itemBuilder: (context, index) {
              final item = _filteredItems[index];
              return ListTile(
                title: Text(item['name']!),
                subtitle: Text(item['category']!),
              );
            },
          ),
        ),
      ],
    );
  }
}

class CategoryFilter extends StatelessWidget {
  final List<String> categories;
  final String selected;
  final ValueChanged<String> onSelected;

  const CategoryFilter({
    super.key,
    required this.categories,
    required this.selected,
    required this.onSelected,
  });

  @override
  Widget build(BuildContext context) {
    return Wrap(
      spacing: 8,
      children: categories.map((cat) {
        return ChoiceChip(
          label: Text(cat),
          selected: selected == cat,
          onSelected: (_) => onSelected(cat),
        );
      }).toList(),
    );
  }
}

مثال عملي: نموذج مع معاينة حية

نموذج مع معاينة - حالة مرفوعة

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

  @override
  State<ProfileEditor> createState() => _ProfileEditorState();
}

class _ProfileEditorState extends State<ProfileEditor> {
  String _name = '';
  String _bio = '';
  Color _themeColor = Colors.blue;

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        // الجانب الأيسر: مدخلات النموذج
        Expanded(
          child: ProfileForm(
            name: _name,
            bio: _bio,
            themeColor: _themeColor,
            onNameChanged: (value) {
              setState(() { _name = value; });
            },
            onBioChanged: (value) {
              setState(() { _bio = value; });
            },
            onColorChanged: (color) {
              setState(() { _themeColor = color; });
            },
          ),
        ),
        // الجانب الأيمن: معاينة حية
        Expanded(
          child: ProfilePreview(
            name: _name,
            bio: _bio,
            themeColor: _themeColor,
          ),
        ),
      ],
    );
  }
}

class ProfileForm extends StatelessWidget {
  final String name;
  final String bio;
  final Color themeColor;
  final ValueChanged<String> onNameChanged;
  final ValueChanged<String> onBioChanged;
  final ValueChanged<Color> onColorChanged;

  const ProfileForm({
    super.key,
    required this.name,
    required this.bio,
    required this.themeColor,
    required this.onNameChanged,
    required this.onBioChanged,
    required this.onColorChanged,
  });

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(16),
      child: Column(
        children: [
          TextField(
            onChanged: onNameChanged,
            decoration: const InputDecoration(labelText: 'الاسم'),
          ),
          const SizedBox(height: 16),
          TextField(
            onChanged: onBioChanged,
            maxLines: 3,
            decoration: const InputDecoration(labelText: 'النبذة'),
          ),
          const SizedBox(height: 16),
          Wrap(
            spacing: 8,
            children: [Colors.blue, Colors.red, Colors.green, Colors.purple]
                .map((color) => GestureDetector(
                      onTap: () => onColorChanged(color),
                      child: CircleAvatar(
                        backgroundColor: color,
                        radius: 20,
                        child: themeColor == color
                            ? const Icon(Icons.check, color: Colors.white)
                            : null,
                      ),
                    ))
                .toList(),
          ),
        ],
      ),
    );
  }
}

class ProfilePreview extends StatelessWidget {
  final String name;
  final String bio;
  final Color themeColor;

  const ProfilePreview({
    super.key,
    required this.name,
    required this.bio,
    required this.themeColor,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.all(16),
      child: Container(
        padding: const EdgeInsets.all(24),
        decoration: BoxDecoration(
          border: Border(top: BorderSide(color: themeColor, width: 4)),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            CircleAvatar(
              backgroundColor: themeColor,
              radius: 30,
              child: Text(
                name.isNotEmpty ? name[0].toUpperCase() : '?',
                style: const TextStyle(
                  color: Colors.white,
                  fontSize: 24,
                ),
              ),
            ),
            const SizedBox(height: 12),
            Text(
              name.isNotEmpty ? name : 'اسمك',
              style: TextStyle(
                fontSize: 20,
                fontWeight: FontWeight.bold,
                color: themeColor,
              ),
            ),
            const SizedBox(height: 8),
            Text(
              bio.isNotEmpty ? bio : 'نبذتك ستظهر هنا...',
              style: TextStyle(
                color: Colors.grey[600],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
النقطة الرئيسية: رفع الحالة لأعلى هو النمط الأساسي لمشاركة الحالة بين الودجات في Flutter. الأب يمتلك الحالة، يمرر البيانات للأسفل كمعاملات مُنشئ، ويتلقى طلبات التغيير عبر ردود الاستدعاء. هذا يضمن مصدراً واحداً للحقيقة وتدفق بيانات أحادي الاتجاه. عندما يصبح حفر الخصائص مفرطاً، فكر في استخدام InheritedWidget أو حزمة إدارة حالة.