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

ما هي الحالة (State) في Flutter؟

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

فهم الحالة (State)

في Flutter، الحالة (State) هي أي بيانات يمكن أن تتغير خلال عمر الودجت. عندما تتغير الحالة، يقوم الإطار بإعادة بناء الأجزاء المتأثرة من واجهة المستخدم لتعكس تلك التغييرات. هذا هو المبدأ الأساسي وراء نموذج البرمجة التفاعلية في Flutter: UI = f(state). واجهة المستخدم الخاصة بك هي دائماً دالة للحالة الحالية.

في كل مرة تتغير فيها جزء من الحالة، يحدد Flutter الودجات التي تعتمد على تلك الحالة ويطلق إعادة بناء لتلك الودجات. هذا يعني أنك لا تحتاج أبداً للتلاعب يدوياً بـ DOM أو التسلسل الهرمي للعرض — ببساطة تقوم بتحديث الحالة وتدع Flutter يتعامل مع الباقي.

ملاحظة: الحالة ليست نفس المتغير. المتغير العادي يحتفظ بالبيانات، لكن الحالة تشير تحديداً إلى البيانات التي عند تغييرها يجب أن تتسبب في تحديث واجهة المستخدم. يحتاج Flutter لمعرفة تغييرات الحالة لإعادة بناء الودجات بشكل صحيح.

الحالة المؤقتة مقابل حالة التطبيق

توثيق Flutter يميز بين فئتين واسعتين من الحالة:

الحالة المؤقتة (الحالة المحلية)

الحالة المؤقتة هي حالة تخص ودجت واحد ولا تحتاج للمشاركة. هي قصيرة العمر، محتواة، وتُدار بالكامل داخل StatefulWidget واحد. تشمل الأمثلة:

  • فهرس الصفحة الحالية في PageView
  • ما إذا كان مربع الاختيار محدداً أم لا
  • القيمة الحالية لحقل نصي
  • تقدم الرسوم المتحركة
  • ما إذا كانت القائمة المنسدلة مفتوحة أم مغلقة

مثال على الحالة المؤقتة

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

  @override
  State<ToggleButton> createState() => _ToggleButtonState();
}

class _ToggleButtonState extends State<ToggleButton> {
  // هذه حالة مؤقتة - فقط هذا الودجت يهتم بها
  bool _isOn = false;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {
        setState(() {
          _isOn = !_isOn;
        });
      },
      style: ElevatedButton.styleFrom(
        backgroundColor: _isOn ? Colors.green : Colors.red,
      ),
      child: Text(_isOn ? 'تشغيل' : 'إيقاف'),
    );
  }
}

حالة التطبيق (الحالة المشتركة)

حالة التطبيق هي حالة تحتاج للمشاركة عبر ودجات متعددة أو حتى عبر التطبيق بالكامل. عادةً ما تتجاوز عمر الودجات الفردية وتؤثر على أجزاء كثيرة من واجهة المستخدم. تشمل الأمثلة:

  • حالة مصادقة المستخدم (مسجل دخول أو خارج)
  • محتويات سلة التسوق
  • تفضيلات المستخدم (السمة، اللغة، حجم الخط)
  • عدد الإشعارات
  • البيانات المجلوبة من API

مثال مفاهيمي لحالة التطبيق

// هذه أمثلة على حالة التطبيق - مشتركة عبر ودجات كثيرة
class AppState {
  final User? currentUser;         // حالة المصادقة تؤثر على شاشات كثيرة
  final List<CartItem> cartItems;  // السلة تُعرض في أماكن متعددة
  final ThemeMode themeMode;       // السمة تؤثر على التطبيق بالكامل
  final String locale;             // اللغة تؤثر على كل النصوص

  const AppState({
    this.currentUser,
    this.cartItems = const [],
    this.themeMode = ThemeMode.system,
    this.locale = 'en',
  });
}
نصيحة: إذا لم تكن متأكداً ما إذا كان شيء ما حالة مؤقتة أو حالة تطبيق، اسأل نفسك: “هل يحتاج أي ودجت آخر لمعرفة هذه البيانات؟” إذا كانت الإجابة نعم، فهي حالة تطبيق. إذا كان الودجت الحالي فقط يهتم، فهي حالة مؤقتة.

شرح UI = f(state)

الصيغة UI = f(state) تعني أن واجهة المستخدم هي دالة نقية لحالة التطبيق. بنفس الحالة، ستبدو واجهة المستخدم دائماً بنفس الشكل. هذا تحول جوهري من برمجة واجهة المستخدم الأمرية حيث تقوم بتحديث العروض الفردية يدوياً.

الأسلوب الأمري مقابل التصريحي

// الأسلوب الأمري (النهج التقليدي - ليس كيف يعمل Flutter)
// تقوم بتحديث كل عنصر واجهة مستخدم يدوياً:
// nameLabel.setText("إدريس");
// avatarImage.setVisible(true);
// loginButton.setVisible(false);

// الأسلوب التصريحي (نهج Flutter)
// تصف كيف يجب أن تبدو واجهة المستخدم بناءً على الحالة الحالية:
@override
Widget build(BuildContext context) {
  return Column(
    children: [
      if (user != null) ...[
        Text(user!.name),          // يُعرض عند تسجيل الدخول
        CircleAvatar(
          backgroundImage: NetworkImage(user!.avatarUrl),
        ),
      ] else ...[
        const Text('مرحباً، زائر'),
        ElevatedButton(
          onPressed: _login,
          child: const Text('تسجيل الدخول'),
        ),
      ],
    ],
  );
}

متى تتسبب تغييرات الحالة في إعادة البناء

لا يكتشف Flutter تغييرات المتغيرات تلقائياً. يجب عليك إخبار Flutter صراحةً بأن الحالة قد تغيرت عن طريق استدعاء setState() داخل StatefulWidget. عند استدعاء setState()، يحدث التالي:

  1. يتم تنفيذ الإغلاق (closure) الذي تمرره إلى setState()، مما يعدل متغيرات الحالة
  2. يقوم Flutter بتعليم الودجت كـ “متسخ” (يحتاج إعادة بناء)
  3. في الإطار التالي، يستدعي Flutter طريقة build() مرة أخرى
  4. يقارن Flutter شجرة الودجات الجديدة مع السابقة
  5. فقط الأجزاء المتغيرة من واجهة المستخدم يتم تحديثها على الشاشة

setState يطلق إعادة البناء

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

  @override
  State<CounterWidget> createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  int _count = 0;

  void _increment() {
    // بدون setState، لن تتحدث واجهة المستخدم
    setState(() {
      _count++;
    });
    // بعد setState، سيتم استدعاء build() مرة أخرى
  }

  @override
  Widget build(BuildContext context) {
    // هذه الطريقة تعمل في كل مرة يتم فيها استدعاء setState
    print('بناء CounterWidget مع العد: \$_count');
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(
          'العد: \$_count',
          style: Theme.of(context).textTheme.headlineMedium,
        ),
        const SizedBox(height: 16),
        ElevatedButton(
          onPressed: _increment,
          child: const Text('زيادة'),
        ),
      ],
    );
  }
}
تحذير: إذا قمت بتغيير متغير بدون استدعاء setState()، سيتم تحديث المتغير في الذاكرة لكن واجهة المستخدم لن تعكس التغيير. هذا أحد أكثر الأخطاء شيوعاً لمبتدئي Flutter.

مراجعة StatefulWidget مقابل StatelessWidget

فهم الفرق بين هذين النوعين من الودجات أمر ضروري لإدارة الحالة:

StatelessWidget

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

مثال StatelessWidget

class UserCard extends StatelessWidget {
  final String name;
  final String email;
  final String avatarUrl;

  const UserCard({
    super.key,
    required this.name,
    required this.email,
    required this.avatarUrl,
  });

  @override
  Widget build(BuildContext context) {
    // هذا الودجت ليس لديه حالة داخلية
    // يعرض فقط ما يتلقاه عبر المُنشئ
    return Card(
      child: ListTile(
        leading: CircleAvatar(
          backgroundImage: NetworkImage(avatarUrl),
        ),
        title: Text(name),
        subtitle: Text(email),
      ),
    );
  }
}

StatefulWidget

StatefulWidget يحتفظ بحالة قابلة للتغيير يمكن أن تتغير خلال عمر الودجت. يتكون من فئتين: فئة الودجت (غير قابلة للتغيير) وفئة الحالة (قابلة للتغيير).

أمثلة على أنواع مختلفة من الحالة

دعونا ننظر إلى أمثلة ملموسة على الحالة التي تواجهها في تطبيقات Flutter الحقيقية:

حالة إدخال النموذج

حقول النص ومربعات الاختيار وأزرار الاختيار وأشرطة التمرير كلها تحتفظ بحالة مؤقتة. النص الحالي في حقل أو الخيار المحدد هو حالة محلية يديرها الودجت.

حالة المصادقة

ما إذا كان المستخدم مسجل دخول ومعلومات ملفه الشخصي ورمز الوصول الخاص به تمثل حالة تطبيق. شاشات متعددة تحتاج لمعرفة ما إذا كان المستخدم مصادقاً لعرض المحتوى المناسب.

حالة سلة التسوق

العناصر في السلة والكميات والسعر الإجمالي هي حالة تطبيق مشتركة بين قائمة المنتجات وشاشة السلة وتدفق الدفع.

حالة تفضيل السمة

الوضع الفاتح أو الداكن أو الافتراضي للنظام هو حالة تطبيق تؤثر على كل ودجت في التطبيق. تغيير السمة يجب أن يطلق إعادة بناء شجرة الودجات بالكامل.

أنواع حالة متعددة في تطبيق واحد

class ProductPage extends StatefulWidget {
  final String productId;
  const ProductPage({super.key, required this.productId});

  @override
  State<ProductPage> createState() => _ProductPageState();
}

class _ProductPageState extends State<ProductPage> {
  // حالة مؤقتة - فقط هذا الودجت يهتم
  int _selectedQuantity = 1;
  bool _showDescription = false;
  int _selectedImageIndex = 0;

  // حالة التطبيق ستأتي من حل إدارة الحالة
  // مثل Provider أو Riverpod أو Bloc
  // final cartProvider = ...
  // final authProvider = ...

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('تفاصيل المنتج')),
      body: Column(
        children: [
          // يستخدم حالة مؤقتة لعرض الصور
          ImageCarousel(
            selectedIndex: _selectedImageIndex,
            onIndexChanged: (index) {
              setState(() {
                _selectedImageIndex = index;
              });
            },
          ),
          // يستخدم حالة مؤقتة للكمية
          QuantitySelector(
            quantity: _selectedQuantity,
            onChanged: (qty) {
              setState(() {
                _selectedQuantity = qty;
              });
            },
          ),
          // التبديل هو حالة مؤقتة
          TextButton(
            onPressed: () {
              setState(() {
                _showDescription = !_showDescription;
              });
            },
            child: Text(
              _showDescription ? 'إخفاء التفاصيل' : 'عرض التفاصيل',
            ),
          ),
          if (_showDescription)
            const Text('وصف المنتج الكامل هنا...'),
        ],
      ),
    );
  }
}

نظرة عامة على مشهد إدارة الحالة

مع نمو تطبيقك بما يتجاوز الحالة المؤقتة البسيطة، ستحتاج أدوات أكثر تطوراً. نظام Flutter البيئي يقدم العديد من حلول إدارة الحالة، كل منها بمقايضات مختلفة:

النهجالتعقيدالأفضل لـ
setStateمنخفضودجات بسيطة مع حالة محلية
InheritedWidgetمتوسطتمرير البيانات عبر شجرة الودجات
Providerمتوسطمعظم التطبيقات، موصى به من فريق Flutter
Riverpodمتوسط-عاليآمن الأنواع، قابل للاختبار، آمن الترجمة
Bloc / Cubitعاليتطبيقات كبيرة، بنية صارمة
GetXمنخفضنماذج أولية سريعة (مثير للجدل)
Reduxعاليحالة يمكن التنبؤ بها، تصحيح السفر عبر الزمن
نصيحة: لا يوجد حل واحد “أفضل” لإدارة الحالة. ابدأ بـ setState للحالات البسيطة، تعلم InheritedWidget لفهم كيف يمرر Flutter البيانات عبر الشجرة، ثم اعتمد Provider أو Riverpod مع نمو تطبيقك. في سلسلة الدروس هذه، سنغطي كل نهج خطوة بخطوة.
النقطة الرئيسية: الحالة هي البيانات التي تقود واجهة المستخدم. الحالة المؤقتة تعيش في ودجت واحد. حالة التطبيق مشتركة عبر ودجات متعددة. يعيد Flutter بناء واجهة المستخدم عند تغير الحالة، ويجب عليك استخدام الآلية المناسبة (مثل setState) لإخطار Flutter بتلك التغييرات.