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

الغوص العميق في setState

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

كيف يعمل setState داخلياً

عند استدعاء setState()، لا يقوم Flutter بإعادة بناء الودجت فوراً. بدلاً من ذلك، يتبع عملية محددة جيداً ومُحسّنة للأداء. فهم هذه العملية سيساعدك في كتابة كود أكثر كفاءة وتجنب المزالق الشائعة.

إليك ما يحدث خطوة بخطوة عند استدعاء setState():

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

داخل setState - كود مصدري مبسط

// هذه نسخة مبسطة مما يفعله setState داخلياً
// من كود مصدر إطار عمل Flutter (framework.dart)

@protected
void setState(VoidCallback fn) {
  // 1. التأكد من أن State لا يزال مُثبتاً
  assert(() {
    if (!mounted) {
      throw FlutterError(
        'setState() called after dispose()',
      );
    }
    return true;
  }());

  // 2. تنفيذ رد الاستدعاء بشكل متزامن
  final Object? result = fn() as dynamic;

  // 3. التأكد من أن رد الاستدعاء لم يُرجع Future
  assert(() {
    if (result is Future) {
      throw FlutterError(
        'setState() callback argument returned a Future.\n'
        'The setState() method on State does not await Futures.',
      );
    }
    return true;
  }());

  // 4. تعليم العنصر كيحتاج إعادة بناء
  _element!.markNeedsBuild();
}
ملاحظة: استدعاء markNeedsBuild() هو الآلية الرئيسية. يخبر خط أنابيب العرض في Flutter أن طريقة build() لهذا العنصر تحتاج للاستدعاء مرة أخرى. إعادة البناء الفعلية تحدث بشكل غير متزامن في الإطار التالي، وليس فوراً عند استدعاء setState().

متى تستدعي setState

يجب استدعاء setState() كلما احتجت لتغيير حالة تؤثر على واجهة المستخدم. ومع ذلك، هناك إرشادات مهمة حول متى وأين تستدعيها:

الاستخدام الصحيح

أنماط setState الصحيحة

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

  @override
  State<GoodExamples> createState() => _GoodExamplesState();
}

class _GoodExamplesState extends State<GoodExamples> {
  int _counter = 0;
  String _status = 'خامل';
  List<String> _items = [];

  // جيد: تحديث حالة بسيط
  void _increment() {
    setState(() {
      _counter++;
    });
  }

  // جيد: تغييرات حالة متعددة في setState واحد
  void _reset() {
    setState(() {
      _counter = 0;
      _status = 'إعادة تعيين';
      _items = [];
    });
  }

  // جيد: تحديث حالة مشروط
  void _incrementIfBelow(int max) {
    if (_counter < max) {
      setState(() {
        _counter++;
      });
    }
  }

  // جيد: عملية غير متزامنة مع setState بعد await
  Future<void> _fetchData() async {
    setState(() {
      _status = 'جاري التحميل';
    });

    final data = await ApiService.fetchItems();

    // تحقق من mounted قبل استدعاء setState بعد await
    if (mounted) {
      setState(() {
        _items = data;
        _status = 'تم التحميل';
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Text('العداد: \$_counter'),
        Text('الحالة: \$_status'),
        Text('العناصر: \${_items.length}'),
      ],
    );
  }
}

ماذا تضع داخل setState

الإغلاق الذي تمرره إلى setState() يجب أن يحتوي فقط على تغييرات الحالة الفعلية. اجعله بسيطاً ومتزامناً. العمليات الحسابية الثقيلة أو العمليات غير المتزامنة يجب أن تحدث خارج الإغلاق.

ما يوضع داخل مقابل خارج

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

  @override
  State<SetStateContent> createState() => _SetStateContentState();
}

class _SetStateContentState extends State<SetStateContent> {
  List<int> _numbers = [];
  String _result = '';

  void _processData() {
    // قم بالحسابات خارج setState
    final filtered = _numbers.where((n) => n > 10).toList();
    final sum = filtered.fold<int>(0, (a, b) => a + b);
    final average = filtered.isEmpty ? 0 : sum / filtered.length;

    // ضع فقط الإسناد داخل setState
    setState(() {
      _result = 'المجموع: \$sum، المتوسط: \${average.toStringAsFixed(2)}';
    });
  }

  // سيئ - عمل كثير داخل setState
  void _processDataBad() {
    setState(() {
      // لا تقم بحسابات ثقيلة داخل setState
      final filtered = _numbers.where((n) => n > 10).toList();
      final sum = filtered.fold<int>(0, (a, b) => a + b);
      final average = filtered.isEmpty ? 0 : sum / filtered.length;
      _result = 'المجموع: \$sum، المتوسط: \${average.toStringAsFixed(2)}';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Text(_result);
  }
}
نصيحة: تقنياً، setState() يستدعي فقط الإغلاق ثم يعلم العنصر كمتسخ. يمكنك حتى استدعاء setState(() {}) بإغلاق فارغ بعد تعديل الحالة. ومع ذلك، وضع التغيير داخل الإغلاق هو العرف لأنه يوصل النية بوضوح ويضمن تشغيل إعادة البناء دائماً.

الأخطاء الشائعة

هناك عدة مزالق يواجهها مطورو Flutter بشكل متكرر مع setState():

الخطأ 1: setState غير متزامن

setState غير متزامن - الطريقة الخاطئة

// سيئ: تمرير إغلاق async إلى setState
void _loadData() {
  setState(() async {
    // هذا خاطئ! setState لا ينتظر Futures
    final data = await http.get(Uri.parse('https://api.example.com/data'));
    _items = jsonDecode(data.body);
  });
  // سيطرح Flutter خطأ في وضع التصحيح:
  // "setState() callback argument returned a Future"
}

// جيد: افصل العملية غير المتزامنة عن setState
Future<void> _loadData() async {
  setState(() {
    _isLoading = true;
  });

  try {
    final data = await http.get(Uri.parse('https://api.example.com/data'));
    final items = jsonDecode(data.body) as List;

    if (mounted) {
      setState(() {
        _items = items.cast<String>();
        _isLoading = false;
      });
    }
  } catch (e) {
    if (mounted) {
      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }
}

الخطأ 2: setState بعد dispose

setState بعد dispose - الانهيار

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

  @override
  State<TimerWidget> createState() => _TimerWidgetState();
}

class _TimerWidgetState extends State<TimerWidget> {
  int _seconds = 0;
  Timer? _timer;

  @override
  void initState() {
    super.initState();
    _timer = Timer.periodic(
      const Duration(seconds: 1),
      (timer) {
        // سيئ: إذا تمت إزالة الودجت أثناء تشغيل المؤقت،
        // سينهار بـ "setState() called after dispose()"
        // setState(() {
        //   _seconds++;
        // });

        // جيد: تحقق دائماً من mounted أولاً
        if (mounted) {
          setState(() {
            _seconds++;
          });
        }
      },
    );
  }

  @override
  void dispose() {
    _timer?.cancel(); // ألغِ المؤقتات دائماً!
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Text('الوقت المنقضي: \$_seconds ثانية');
  }
}
تحذير: خاصية mounted تكون true بعد initState() وfalse بعد dispose(). تحقق دائماً من mounted قبل استدعاء setState() في ردود الاستدعاء التي قد تُطلق بعد إزالة الودجت من الشجرة (المؤقتات، اشتراكات التدفق، العمليات غير المتزامنة).

الخطأ 3: استدعاء setState في build

لا تستدعي setState في build أبداً

// سيئ: هذا ينشئ حلقة لا نهائية!
@override
Widget build(BuildContext context) {
  // setState يطلق build، وbuild يستدعي setState، الذي يطلق build...
  // setState(() { _counter++; }); // لا تفعل هذا أبداً

  // جيد: استخدم حالة مشتقة من الحالة الموجودة بدون setState
  final displayText = _counter > 10 ? 'عالي' : 'منخفض';

  return Text(displayText);
}

فحص mounted

خاصية mounted حاسمة لإدارة الحالة الآمنة. تشير إلى ما إذا كان كائن State مرتبطاً حالياً بعنصر في شجرة الودجات.

نمط آمن للعمليات غير المتزامنة مع mounted

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

  @override
  State<SafeAsyncWidget> createState() => _SafeAsyncWidgetState();
}

class _SafeAsyncWidgetState extends State<SafeAsyncWidget> {
  String _data = '';
  bool _isLoading = false;
  String? _error;

  @override
  void initState() {
    super.initState();
    _loadData();
  }

  Future<void> _loadData() async {
    setState(() {
      _isLoading = true;
      _error = null;
    });

    try {
      // محاكاة تأخير الشبكة
      await Future.delayed(const Duration(seconds: 2));
      final result = 'تم تحميل البيانات بنجاح';

      // قد يكون الودجت قد تم التخلص منه أثناء التأخير
      if (!mounted) return;

      setState(() {
        _data = result;
        _isLoading = false;
      });
    } catch (e) {
      if (!mounted) return;

      setState(() {
        _error = e.toString();
        _isLoading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (_isLoading) {
      return const CircularProgressIndicator();
    }
    if (_error != null) {
      return Text('خطأ: \$_error');
    }
    return Text(_data);
  }
}

تأثيرات الأداء

كل استدعاء لـ setState() يطلق إعادة بناء الشجرة الفرعية الكاملة المتجذرة في StatefulWidget. هذا يعني أنه يجب أن تكون واعياً لمكان وضع حالتك:

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

// سيئ: الصفحة بالكامل تُعاد بناءها عند تغيير العداد
class EntirePage extends StatefulWidget {
  const EntirePage({super.key});

  @override
  State<EntirePage> createState() => _EntirePageState();
}

class _EntirePageState extends State<EntirePage> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    print('إعادة بناء الصفحة بالكامل');
    return Scaffold(
      appBar: AppBar(title: const Text('تطبيقي')),
      body: Column(
        children: [
          const ExpensiveHeader(),    // يُعاد بناؤه بلا داعٍ!
          const ExpensiveList(),      // يُعاد بناؤه بلا داعٍ!
          Text('العداد: \$_counter'), // فقط هذا يحتاج العداد
          ElevatedButton(
            onPressed: () => setState(() => _counter++),
            child: const Text('زيادة'),
          ),
        ],
      ),
    );
  }
}

// جيد: فقط ودجت العداد يُعاد بناؤه
class OptimizedPage extends StatelessWidget {
  const OptimizedPage({super.key});

  @override
  Widget build(BuildContext context) {
    print('بناء الصفحة (مرة واحدة فقط)');
    return Scaffold(
      appBar: AppBar(title: const Text('تطبيقي')),
      body: Column(
        children: const [
          ExpensiveHeader(),    // لا يُعاد بناؤه أبداً
          ExpensiveList(),      // لا يُعاد بناؤه أبداً
          CounterSection(),     // فقط هذا يُعاد بناؤه
        ],
      ),
    );
  }
}

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

  @override
  State<CounterSection> createState() => _CounterSectionState();
}

class _CounterSectionState extends State<CounterSection> {
  int _counter = 0;

  @override
  Widget build(BuildContext context) {
    print('إعادة بناء قسم العداد فقط');
    return Column(
      children: [
        Text('العداد: \$_counter'),
        ElevatedButton(
          onPressed: () => setState(() => _counter++),
          child: const Text('زيادة'),
        ),
      ],
    );
  }
}

تجميع التحديثات

استدعاءات متعددة لـ setState() ضمن نفس التنفيذ المتزامن ستؤدي إلى إعادة بناء واحدة فقط. يقوم Flutter بتجميع العناصر المتسخة وإعادة بنائها مرة واحدة في الإطار التالي.

استدعاءات setState مجمعة مقابل منفصلة

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

  @override
  State<BatchingExample> createState() => _BatchingExampleState();
}

class _BatchingExampleState extends State<BatchingExample> {
  int _a = 0;
  int _b = 0;
  int _c = 0;

  // هذه الاستدعاءات الثلاثة تؤدي إلى إعادة بناء واحدة، وليس ثلاث
  void _updateAll() {
    setState(() { _a++; });
    setState(() { _b++; });
    setState(() { _c++; });
    // سيتم استدعاء build() مرة واحدة في الإطار التالي
  }

  // أفضل: ادمجها في setState واحد للوضوح
  void _updateAllBetter() {
    setState(() {
      _a++;
      _b++;
      _c++;
    });
  }

  @override
  Widget build(BuildContext context) {
    print('تم استدعاء Build - a:\$_a, b:\$_b, c:\$_c');
    return Text('a:\$_a  b:\$_b  c:\$_c');
  }
}
نصيحة: حتى وإن كانت استدعاءات setState() المتعددة تُجمع في إعادة بناء واحدة، من الأنظف والأسهل قراءة دمج تغييرات الحالة المرتبطة في استدعاء setState() واحد. هذا يجعل الكود أسهل في الفهم والتصحيح.

أمثلة عملية

عداد بعمليات متعددة

ودجت عداد متقدم

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

  @override
  State<AdvancedCounter> createState() => _AdvancedCounterState();
}

class _AdvancedCounterState extends State<AdvancedCounter> {
  int _count = 0;
  int _step = 1;
  final List<int> _history = [];

  void _increment() {
    setState(() {
      _history.add(_count);
      _count += _step;
    });
  }

  void _decrement() {
    setState(() {
      _history.add(_count);
      _count -= _step;
    });
  }

  void _undo() {
    if (_history.isNotEmpty) {
      setState(() {
        _count = _history.removeLast();
      });
    }
  }

  void _changeStep(int newStep) {
    setState(() {
      _step = newStep;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        Text(
          '\$_count',
          style: Theme.of(context).textTheme.displayLarge,
        ),
        Text('الخطوة: \$_step'),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            IconButton(
              onPressed: _decrement,
              icon: const Icon(Icons.remove),
            ),
            IconButton(
              onPressed: _increment,
              icon: const Icon(Icons.add),
            ),
            IconButton(
              onPressed: _history.isNotEmpty ? _undo : null,
              icon: const Icon(Icons.undo),
            ),
          ],
        ),
        Slider(
          value: _step.toDouble(),
          min: 1,
          max: 10,
          divisions: 9,
          label: 'الخطوة: \$_step',
          onChanged: (v) => _changeStep(v.toInt()),
        ),
      ],
    );
  }
}

مثال تبديل واجهة المستخدم

لوحة تبديل متعددة

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

  @override
  State<SettingsPanel> createState() => _SettingsPanelState();
}

class _SettingsPanelState extends State<SettingsPanel> {
  bool _darkMode = false;
  bool _notifications = true;
  bool _autoSave = false;
  double _fontSize = 16.0;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        SwitchListTile(
          title: const Text('الوضع الداكن'),
          value: _darkMode,
          onChanged: (value) {
            setState(() {
              _darkMode = value;
            });
          },
        ),
        SwitchListTile(
          title: const Text('الإشعارات'),
          value: _notifications,
          onChanged: (value) {
            setState(() {
              _notifications = value;
            });
          },
        ),
        SwitchListTile(
          title: const Text('الحفظ التلقائي'),
          value: _autoSave,
          onChanged: (value) {
            setState(() {
              _autoSave = value;
            });
          },
        ),
        ListTile(
          title: Text('حجم الخط: \${_fontSize.toInt()}'),
          subtitle: Slider(
            value: _fontSize,
            min: 12,
            max: 24,
            onChanged: (value) {
              setState(() {
                _fontSize = value;
              });
            },
          ),
        ),
        // معاينة بالإعدادات الحالية
        Container(
          padding: const EdgeInsets.all(16),
          color: _darkMode ? Colors.grey[900] : Colors.white,
          child: Text(
            'نص معاينة بالإعدادات الحالية',
            style: TextStyle(
              fontSize: _fontSize,
              color: _darkMode ? Colors.white : Colors.black,
            ),
          ),
        ),
      ],
    );
  }
}
النقطة الرئيسية: setState() هو أبسط آلية لإدارة الحالة في Flutter. يعمل عن طريق تعليم العنصر كمتسخ وجدولة إعادة البناء. تحقق دائماً من mounted قبل استدعائه في السياقات غير المتزامنة، ولا تمرر أبداً إغلاقاً غير متزامن إليه، وقلل نطاق الودجت الذي يستدعيه لتحسين الأداء.