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

اختيار النهج الصحيح

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

مشهد إدارة الحالة

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

مهم: لا يوجد حل واحد “أفضل” لإدارة الحالة. الاختيار الصحيح يعتمد على حجم تطبيقك وخبرة فريقك ومتطلبات الأداء ومقدار الكود المتكرر الذي ترغب في كتابته. أي شخص يخبرك “استخدم دائماً X” يبالغ في التبسيط.

الخيارات بنظرة سريعة

إليك مقارنة بين أكثر النهج شيوعاً في نظام Flutter البيئي اعتباراً من Flutter 3.x:

جدول المقارنة

┌──────────────────┬────────────┬────────────┬───────────┬────────────┐
│ النهج            │ التعقيد    │ الكود      │ الأفضل    │ منحنى      │
│                  │            │ المتكرر   │ لـ        │ التعلم     │
├──────────────────┼────────────┼────────────┼───────────┼────────────┤
│ setState         │ منخفض جداً │ أدنى      │ حالة      │ مبتدئ      │
│                  │            │            │ UI محلية  │            │
├──────────────────┼────────────┼────────────┼───────────┼────────────┤
│ InheritedWidget  │ متوسط     │ عالي       │ تعلم      │ متوسط      │
│ / InheritedModel │            │            │ أساسيات   │            │
│                  │            │            │ Flutter   │            │
├──────────────────┼────────────┼────────────┼───────────┼────────────┤
│ Provider         │ منخفض-     │ منخفض     │ تطبيقات   │ مبتدئ-     │
│                  │ متوسط     │            │ صغيرة     │ متوسط      │
│                  │            │            │ إلى كبيرة │            │
├──────────────────┼────────────┼────────────┼───────────┼────────────┤
│ Riverpod         │ متوسط     │ منخفض-     │ تطبيقات   │ متوسط      │
│                  │            │ متوسط     │ متوسطة    │            │
│                  │            │            │ إلى كبيرة │            │
├──────────────────┼────────────┼────────────┼───────────┼────────────┤
│ Bloc / Cubit     │ متوسط-عالي│ عالي       │ تطبيقات   │ متوسط-     │
│                  │            │            │ كبيرة     │ متقدم      │
│                  │            │            │ وفرق      │            │
├──────────────────┼────────────┼────────────┼───────────┼────────────┤
│ GetX             │ منخفض     │ منخفض جداً │ نماذج     │ مبتدئ      │
│                  │            │            │ أولية     │            │
│                  │            │            │ سريعة     │            │
├──────────────────┼────────────┼────────────┼───────────┼────────────┤
│ Redux            │ عالي       │ عالي جداً  │ فرق من    │ متقدم      │
│                  │            │            │ React/web │            │
└──────────────────┴────────────┴────────────┴───────────┴────────────┘

إطار اتخاذ القرار

اسأل نفسك هذه الأسئلة بالترتيب. كل إجابة تضيق خياراتك:

شجرة القرار

س1: هل الحالة تُستخدم بواسطة ودجت واحدة فقط؟
  نعم --> استخدم setState. انتهى.
  لا  --> تابع إلى س2.

س2: هل الحالة مشتركة بين عدد قليل من الودجات القريبة (أب-ابن)؟
  نعم --> فكر في التمرير عبر المُنشئ أو استخدام
          InheritedWidget / Provider على مستوى الشجرة الفرعية.
  لا  --> تابع إلى س3.

س3: ما حجم تطبيقك / فريقك؟
  ┌─────────────────────────────────────────────────────┐
  │ تطبيق صغير (1-2 مطور، <20 شاشة):                  │
  │   --> Provider هو الخيار الصحيح دائماً تقريباً.    │
  │       بسيط وموثق جيداً وموصى به رسمياً              │
  │                                                     │
  │ تطبيق متوسط (2-5 مطورين، 20-50 شاشة):              │
  │   --> Provider أو Riverpod.                         │
  │       Riverpod يضيف أماناً وقت الترجمة واختبارية    │
  │       أفضل. Provider أبسط للتعلم.                   │
  │                                                     │
  │ تطبيق كبير (5+ مطورين، 50+ شاشة):                   │
  │   --> Bloc أو Riverpod.                             │
  │       Bloc يفرض أنماطاً صارمة تساعد الفرق الكبيرة  │
  │       على البقاء متسقة. Riverpod أكثر مرونة         │
  │       لكن يتطلب انضباط الفريق.                      │
  └─────────────────────────────────────────────────────┘

س4: هل تحتاج تتبعاً صارماً للأحداث / تصحيح أخطاء بالسفر عبر الزمن؟
  نعم --> Bloc (الأحداث من الدرجة الأولى ومسجلة وقابلة لإعادة التشغيل)
  لا  --> Provider أو Riverpod أبسط.

س5: هل فريقك لديه خبرة في React/Redux؟
  نعم --> قد يفضلون Bloc (نمط مشابه مدفوع بالأحداث)
          أو Redux (مألوف لكن مطول في Dart).
  لا  --> تجنب Redux. Provider أو Riverpod أكثر طبيعية في Dart.

نظرة معمقة: متى تستخدم كل واحد

setState -- الأساس

استخدم setState لحالة واجهة المستخدم المحلية والمؤقتة حقاً. ليست نهجاً “سيئاً” -- إنها النهج الصحيح للحالة الصحيحة.

setState: حالات استخدام مثالية

// مثالي لـ setState:
// - تبديل الرسوم المتحركة
// - رؤية حقل النموذج
// - فتح/إغلاق الصفحة السفلية
// - اختيار علامة التبويب
// - قسم قابل للتوسيع

class SearchBar extends StatefulWidget {
  @override
  State<SearchBar> createState() => _SearchBarState();
}

class _SearchBarState extends State<SearchBar> {
  bool _isExpanded = false;
  final _controller = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return AnimatedContainer(
      duration: const Duration(milliseconds: 300),
      width: _isExpanded ? 300 : 48,
      child: Row(
        children: [
          IconButton(
            icon: const Icon(Icons.search),
            onPressed: () => setState(() => _isExpanded = !_isExpanded),
          ),
          if (_isExpanded)
            Expanded(
              child: TextField(controller: _controller),
            ),
        ],
      ),
    );
  }

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

// خطأ لـ setState:
// - حالة مصادقة المستخدم (مطلوبة عبر التطبيق)
// - سلة التسوق (مشتركة عبر الشاشات)
// - تفضيل المظهر (يؤثر على التطبيق بأكمله)
// - بيانات API التي تعرضها ودجات متعددة

Provider -- النقطة المثالية

Provider هو الحل الموصى به رسمياً من فريق Flutter. يغلف InheritedWidget في واجهة برمجة نظيفة وسهلة الاستخدام. يتوسع من التطبيقات الصغيرة إلى الكبيرة ولديه توثيق ممتاز.

لماذا Provider يفوز لمعظم التطبيقات: بسيط بما يكفي للمبتدئين وقوي بما يكفي للإنتاج ولديه أكبر مجتمع وأكثر الدروس التعليمية ويصانه Remi Rousselet (الذي أنشأ Riverpod أيضاً). إذا لم تكن متأكداً ابدأ بـ Provider.

Riverpod -- تطور Provider

أنشأه نفس مؤلف Provider. Riverpod يصلح قيود Provider: أمان وقت الترجمة (لا ProviderNotFoundException وقت التشغيل) ولا اعتماد على BuildContext لقراءة الحالة واختبار أفضل والتخلص التلقائي من المزود.

Provider مقابل Riverpod: الاختلافات الرئيسية

// PROVIDER: يعتمد على BuildContext وأخطاء وقت التشغيل ممكنة
class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    // يرمي خطأ وقت التشغيل إذا لم يُقدم AuthNotifier أعلاه
    final auth = context.watch<AuthNotifier>();
    return Text(auth.state.user?.name ?? 'ضيف');
  }
}

// RIVERPOD: لا حاجة لـ BuildContext وآمن وقت الترجمة
final authProvider = ChangeNotifierProvider((ref) => AuthNotifier());

class MyWidget extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    // آمن وقت الترجمة -- authProvider تصريح عام
    final auth = ref.watch(authProvider);
    return Text(auth.state.user?.name ?? 'ضيف');
  }
}

// RIVERPOD: يمكن قراءة المزودين بدون BuildContext (رائع للاختبار)
void main() {
  final container = ProviderContainer();
  final auth = container.read(authProvider);
  // لا حاجة لشجرة ودجات!
}

Bloc -- هيكل مستوى المؤسسات

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

Bloc: أحداث وحالات منظمة

// Bloc يتطلب أحداثاً صريحة -- كود أكثر وتتبعية أكثر
// كل تغيير في الحالة يرتبط بحدث واحد بالضبط

// الأحداث -- ما يمكن أن يحدث
abstract class TodoEvent {}
class AddTodo extends TodoEvent {
  final String title;
  AddTodo(this.title);
}
class ToggleTodo extends TodoEvent {
  final String id;
  ToggleTodo(this.id);
}
class DeleteTodo extends TodoEvent {
  final String id;
  DeleteTodo(this.id);
}

// الحالات -- ما تعرضه واجهة المستخدم
class TodoState {
  final List<Todo> todos;
  final bool isLoading;
  const TodoState({this.todos = const [], this.isLoading = false});
}

// Bloc -- يربط الأحداث بتغييرات الحالة
class TodoBloc extends Bloc<TodoEvent, TodoState> {
  TodoBloc() : super(const TodoState()) {
    on<AddTodo>((event, emit) {
      emit(TodoState(
        todos: [...state.todos, Todo(title: event.title)],
      ));
    });

    on<ToggleTodo>((event, emit) {
      emit(TodoState(
        todos: state.todos.map((t) =>
          t.id == event.id ? t.copyWith(isCompleted: !t.isCompleted) : t
        ).toList(),
      ));
    });
  }
}

// الاستخدام في الودجت
BlocBuilder<TodoBloc, TodoState>(
  builder: (context, state) {
    return ListView(
      children: state.todos.map((t) => TodoTile(todo: t)).toList(),
    );
  },
)

// إرسال الأحداث
context.read<TodoBloc>().add(AddTodo('اشتري حليب'));
context.read<TodoBloc>().add(ToggleTodo('abc123'));

مسارات الترحيل

لا يجب أن تلتزم بحل واحد للأبد. إليك مسارات الترحيل الشائعة:

مسارات الترحيل:

setState → Provider: أسهل ترحيل. استخرج الحالة إلى فئات ChangeNotifier وغلف تطبيقك بـ MultiProvider واستبدل setState بـ notifyListeners. يمكن القيام به شاشة بشاشة.

Provider → Riverpod: جهد معتدل. استبدل ChangeNotifierProvider بمكافئات Riverpod وغير context.watch إلى ref.watch وأزل اعتماديات BuildContext. حزمة Riverpod توفر أدوات ترحيل.

Provider → Bloc: جهد كبير. أعد تصميم تدفق الحالة بالأحداث والحالات. يستحق العناء إذا كنت تحتاج تتبعية صارمة للأحداث لفريق متنامٍ.

أي → أي: دائماً ممكن لأن طبقة ودجات Flutter تبقى نفسها. فقط طبقة إدارة الحالة تتغير. هذا هو سبب أهمية فصل الاهتمامات -- إذا كان منطق أعمالك بالفعل في فئات منفصلة فتغيير الأطر أسهل بكثير.

عندما يكون البسيط أفضل

قاوم الرغبة في استخدام الحل الأكثر “تقدماً”. التعقيد له تكلفة حقيقية: أخطاء أكثر وتأهيل أبطأ وتصحيح أخطاء أصعب. إليك علامات أنك تبالغ في الهندسة:

علامات المبالغة في الهندسة:
- تكتب 5+ ملفات لإدارة علم منطقي واحد
- إعداد إدارة الحالة يستغرق وقتاً أطول من الميزة نفسها
- المطورون المبتدئون لا يستطيعون فهم تدفق البيانات بعد 30 دقيقة من الشرح
- لديك كود متكرر أكثر من منطق الأعمال الفعلي
- اخترت Bloc لتطبيق من 5 شاشات بدون قواعد أعمال معقدة
- تستخدم Redux عندما لا أحد في الفريق لديه خبرة React

أمثلة عملية

مثال 1: تطبيق صغير (مهام شخصية) -- استخدم Provider

// مثالي لـ Provider:
// - 5-10 شاشات
// - 1-2 مطورين
// - عمليات CRUD بسيطة
// - لا سير عمل غير متزامن معقد

// main.dart -- إعداد بسيط
void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => TodoNotifier(),
      child: const TodoApp(),
    ),
  );
}

// ملف واحد لكل منطق المهام -- بسيط وواضح
class TodoNotifier extends ChangeNotifier {
  final List<Todo> _todos = [];

  List<Todo> get todos => List.unmodifiable(_todos);

  void add(String title) {
    _todos.add(Todo(title: title));
    notifyListeners();
  }

  void toggle(int index) {
    _todos[index] = _todos[index].copyWith(
      isCompleted: !_todos[index].isCompleted,
    );
    notifyListeners();
  }

  void remove(int index) {
    _todos.removeAt(index);
    notifyListeners();
  }
}

مثال 2: تطبيق متوسط (تجارة إلكترونية) -- استخدم Provider أو Riverpod

// جيد لـ Provider مع هيكل مناسب:
// - 20-40 شاشة
// - 2-4 مطورين
// - مناطق حالة مشتركة متعددة (مصادقة وسلة ومنتجات)
// - بعض سير العمل غير المتزامن

// منظم بمزودين متعددين
void main() {
  runApp(
    MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) => AuthNotifier(AuthRepository())),
        ChangeNotifierProvider(create: (_) => CartNotifier()),
        ChangeNotifierProvider(create: (_) => ProductNotifier(ProductRepository())),
        ChangeNotifierProvider(create: (_) => ThemeNotifier()),
      ],
      child: const ShopApp(),
    ),
  );
}

// كل مُعلم يتعامل مع مجال واحد
// أبقها مركزة ومستقلة

مثال 3: تطبيق كبير (بنكي/مؤسساتي) -- استخدم Bloc

// Bloc يتألق لـ:
// - 50+ شاشة
// - 5+ مطورين
// - سير عمل غير متزامن معقد (معاملات وبيانات فورية)
// - متطلبات تدقيق/تتبعية صارمة
// - الحاجة لإعادة تشغيل أو تسجيل كل تغيير حالة

// كل إجراء هو حدث مكتوب وقابل للتتبع
class TransferBloc extends Bloc<TransferEvent, TransferState> {
  final TransferRepository _repo;
  final AuditLogger _logger;

  TransferBloc(this._repo, this._logger)
      : super(TransferInitial()) {

    on<TransferInitiated>((event, emit) async {
      _logger.log('بدء التحويل: \${event.amount} إلى \${event.recipient}');
      emit(TransferProcessing());

      try {
        final result = await _repo.transfer(
          amount: event.amount,
          to: event.recipient,
        );
        _logger.log('اكتمل التحويل: \${result.transactionId}');
        emit(TransferSuccess(result));
      } catch (e) {
        _logger.log('فشل التحويل: \$e');
        emit(TransferFailure(e.toString()));
      }
    });
  }
}

// تتبعية كاملة -- تعرف بالضبط ماذا حدث ومتى
// BlocObserver يسجل كل حدث وانتقال حالة عالمياً

الملخص

دليل القرار السريع:
- حالة ودجت واحدة؟setState
- تطبيق صغير إلى متوسط أو تعلم أو بداية؟Provider
- تريد أمان وقت الترجمة واختبار أفضل؟Riverpod
- فريق كبير وسير عمل معقد واحتياجات تدقيق؟Bloc
- نموذج أولي سريع ومطور منفرد؟ → Provider أو حتى GetX
- قادم من React/Redux؟ → Bloc أو Redux

تذكر: يمكنك دائماً الترحيل لاحقاً. ابدأ ببساطة وأضف التعقيد فقط عندما تشعر بألم الحل الحالي. أفضل إدارة للحالة هي تلك التي يفهمها فريقك ويستخدمها بشكل صحيح.