6 طرق لإدارة الحالة في تطبيقات Flutter (مع أمثلة على الكود)

كيفية إدارة الحالة في Flutter

Page content

في هذا المقال، سنستعرض
ستة طرق شائعة لـ إدارة الحالة في Flutter التطبيقات،
بما في ذلك أمثلة حقيقية وممارسات مثلى:

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

الاستراتيجيات لإدارة الحالة في Flutter هي:

  1. 🧱 setState() — الطريقة المدمجة البسيطة
  2. 🏗️ InheritedWidget — الأساس الذي تبني عليه Flutter إدارة الحالة
  3. 🪄 Provider — الحل الموصى به لمعظم التطبيقات
  4. 🔒 Riverpod — تطور حديث آمن من حيث التجميع لـ Provider
  5. 📦 Bloc — لتطبيقات موسعة ومستوى مؤسسي
  6. GetX — حل مدمج خفيف الوزن

flutter troubles mega tracktor


1. استخدام setState() — الأساسيات

أبسط طريقة لإدارة الحالة في Flutter هي استخدام الطريقة المدمجة setState() داخل StatefulWidget.

هذه الطريقة مناسبة بشكل مثالي لـ حالة واجهة المستخدم المحلية — حيث تنتمي الحالة إلى وidget واحد ولا تحتاج إلى مشاركةها عبر التطبيق.

مثال: تطبيق عدّاد مع setState()

import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: CounterScreen());
  }
}

class CounterScreen extends StatefulWidget {
  @override
  _CounterScreenState createState() => _CounterScreenState();
}

class _CounterScreenState extends State<CounterScreen> {
  int _count = 0;

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

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('setState Counter')),
      body: Center(
        child: Text('Count: $_count', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

المزايا

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

السلبيات

  • لا تتناسب مع التطبيقات الكبيرة
  • صعبة مشاركة الحالة بين الـwidgets
  • تدمج المنطق مع واجهة المستخدم

استخدمها عندما:

  • تبني نموذجًا أوليًا أو وidgets صغيرة
  • تتعامل مع حالة واجهة المستخدم المنفصلة (مثل تبديل زر، عرض نافذة مُodal)

2. InheritedWidget — الأساس في Flutter

InheritedWidget هي الآلية الأساسية التي يستخدمها Flutter لنقل البيانات عبر شجرة الـwidgets. معظم حلول إدارة الحالة (بما في ذلك Provider) مبنية على أساسها.

فهم InheritedWidget يساعدك على فهم كيفية عمل إدارة الحالة في Flutter من الداخل.

مثال: مدير الموضوع مع InheritedWidget

import 'package:flutter/material.dart';

// InheritedWidget الذي يحتوي على بيانات الموضوع
class AppTheme extends InheritedWidget {
  final bool isDarkMode;
  final Function toggleTheme;

  const AppTheme({
    Key? key,
    required this.isDarkMode,
    required this.toggleTheme,
    required Widget child,
  }) : super(key: key, child: child);

  // طريقة الوصول للحصول على AppTheme الأقرب في شجرة الـwidgets
  static AppTheme? of(BuildContext context) {
    return context.dependOnInheritedWidgetOfExactType<AppTheme>();
  }

  @override
  bool updateShouldNotify(AppTheme oldWidget) {
    return isDarkMode != oldWidget.isDarkMode;
  }
}

// المُحاط لإدارة التغييرات في الحالة
class ThemeManager extends StatefulWidget {
  final Widget child;

  const ThemeManager({Key? key, required this.child}) : super(key: key);

  @override
  _ThemeManagerState createState() => _ThemeManagerState();
}

class _ThemeManagerState extends State<ThemeManager> {
  bool _isDarkMode = false;

  void _toggleTheme() {
    setState(() {
      _isDarkMode = !_isDarkMode;
    });
  }

  @override
  Widget build(BuildContext context) {
    return AppTheme(
      isDarkMode: _isDarkMode,
      toggleTheme: _toggleTheme,
      child: widget.child,
    );
  }
}

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ThemeManager(
      child: Builder(
        builder: (context) {
          final theme = AppTheme.of(context);
          return MaterialApp(
            theme: theme!.isDarkMode ? ThemeData.dark() : ThemeData.light(),
            home: HomeScreen(),
          );
        },
      ),
    );
  }
}

class HomeScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final theme = AppTheme.of(context);
    
    return Scaffold(
      appBar: AppBar(title: Text('InheritedWidget Theme')),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              'Current Theme: ${theme!.isDarkMode ? "Dark" : "Light"}',
              style: TextStyle(fontSize: 20),
            ),
            SizedBox(height: 20),
            ElevatedButton(
              onPressed: () => theme.toggleTheme(),
              child: Text('Toggle Theme'),
            ),
          ],
        ),
      ),
    );
  }
}

المزايا

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

السلبيات

  • كود مُكرر معقد
  • يتطلب مُحاط StatefulWidget
  • سهل ارتكاب الأخطاء
  • ليست مناسبة للمبتدئين

استخدمها عندما:

  • تتعلم كيفية عمل إدارة الحالة في Flutter من الداخل
  • تبني حلول إدارة الحالة المخصصة
  • تحتاج إلى تحكم دقيق جدًا في نقل الحالة

3. Provider — الحل الموصى به في Flutter

عندما تحتاج حالة تطبيقك إلى مشاركة بين عدة وidgets، Provider هو الحل الذي ينقذك.

Provider يعتمد على عكس السيطرة — بدلًا من أن يملك الـwidgets الحالة، يعرضها مزود للاستهلاك من قبل الآخرين. يوصي فريق Flutter به رسميًا لتطبيقات متوسطة الحجم.

الإعداد

أضف هذا إلى ملف pubspec.yaml:

dependencies:
  provider: ^6.0.5

مثال: تطبيق عدّاد مع Provider

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';

void main() {
  runApp(
    ChangeNotifierProvider(
      create: (_) => CounterModel(),
      child: MyApp(),
    ),
  );
}

class CounterModel extends ChangeNotifier {
  int _count = 0;
  int get count => _count;

  void increment() {
    _count++;
    notifyListeners();
  }
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: CounterScreen());
  }
}

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final counter = context.watch<CounterModel>();
    return Scaffold(
      appBar: AppBar(title: Text('Provider Counter')),
      body: Center(
        child: Text('Count: ${counter.count}', style: TextStyle(fontSize: 24)),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: context.read<CounterModel>().increment,
        child: Icon(Icons.add),
      ),
    );
  }
}

المزايا

  • تحديثات تفاعلية وفعالة
  • فصل واضح بين واجهة المستخدم والمنطق
  • مستند جيدًا ومدعوم من قبل المجتمع

السلبيات

  • قليلاً من الكود المُكرر أكثر من setState()
  • يمكن أن تصبح المزودات المُضمنة معقدة

استخدمها عندما:

  • تحتاج إلى مشاركة الحالة بين عدة وidgets
  • ترغب في نمط تفاعلي دون تعقيد
  • ينمو التطبيق عن النموذج الأولي

4. Riverpod — التطور الحديث لـ Provider

Riverpod هو إعادة كتابة كاملة لـ Provider تزيل اعتمادها على BuildContext وتضيف أمانًا في وقت التجميع. تم تصميمها لحل قيود Provider مع الحفاظ على الفلسفة نفسها.

الإعداد

أضف هذا إلى ملف pubspec.yaml:

dependencies:
  flutter_riverpod: ^2.4.0

مثال: ملف تعريف المستخدم مع Riverpod

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

// النموذج
class UserProfile {
  final String name;
  final int age;
  
  UserProfile({required this.name, required this.age});
  
  UserProfile copyWith({String? name, int? age}) {
    return UserProfile(
      name: name ?? this.name,
      age: age ?? this.age,
    );
  }
}

// المُراقب
class ProfileNotifier extends StateNotifier<UserProfile> {
  ProfileNotifier() : super(UserProfile(name: 'Guest', age: 0));

  void updateName(String name) {
    state = state.copyWith(name: name);
  }

  void updateAge(int age) {
    state = state.copyWith(age: age);
  }
}

// المزود
final profileProvider = StateNotifierProvider<ProfileNotifier, UserProfile>((ref) {
  return ProfileNotifier();
});

// المزود المحسوب
final greetingProvider = Provider<String>((ref) {
  final profile = ref.watch(profileProvider);
  return 'Hello, ${profile.name}! You are ${profile.age} years old.';
});

void main() {
  runApp(
    ProviderScope(
      child: MyApp(),
    ),
  );
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(home: ProfileScreen());
  }
}

class ProfileScreen extends ConsumerWidget {
  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final profile = ref.watch(profileProvider);
    final greeting = ref.watch(greetingProvider);

    return Scaffold(
      appBar: AppBar(title: Text('Riverpod Profile')),
      body: Padding(
        padding: EdgeInsets.all(16),
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(greeting, style: TextStyle(fontSize: 20)),
            SizedBox(height: 30),
            TextField(
              decoration: InputDecoration(labelText: 'Name'),
              onChanged: (value) {
                ref.read(profileProvider.notifier).updateName(value);
              },
            ),
            SizedBox(height: 20),
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text('Age: ${profile.age}', style: TextStyle(fontSize: 18)),
                SizedBox(width: 20),
                ElevatedButton(
                  onPressed: () {
                    ref.read(profileProvider.notifier).updateAge(profile.age + 1);
                  },
                  child: Text('+'),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    if (profile.age > 0) {
                      ref.read(profileProvider.notifier).updateAge(profile.age - 1);
                    }
                  },
                  child: Text('-'),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

المزايا

  • لا تعتمد على BuildContext — يمكن الوصول إلى الحالة في أي مكان
  • أمان في وقت التجميع — اكتشاف الأخطاء قبل التشغيل
  • اختبار أفضل
  • تكوين قوي للحالة
  • لا تسرب الذاكرة

السلبيات

  • واجهة برمجة تختلف عن الأنماط التقليدية في Flutter
  • منحنى تعليمي أصعب من Provider
  • مجتمع أصغر (لكن ينمو بسرعة)

استخدمها عندما:

  • تبدأ مشروع Flutter جديد
  • ترغب في الأمان من حيث النوع وضمانات وقت التجميع
  • توجد اعتمادات معقدة وتكوينات للحالة
  • تعمل على تطبيقات متوسطة إلى كبيرة الحجم

5. BLoC (Business Logic Component) — لتطبيقات المؤسسات

نمط BLoC هو نمط متقدم يفصل تمامًا المنطق التجاري عن الواجهة باستخدام التدفق.

وهو مناسب جدًا لـ تطبيقات كبيرة أو مؤسسية، حيث تكون الانتقالات المتوقعة والقابلة للاختبار ضرورية.

الإعداد

أضف هذا الاعتماد إلى مشروعك:

dependencies:
  flutter_bloc: ^9.0.0

مثال: تطبيق عدّاد مع BLoC

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';

// الأحداث
abstract class CounterEvent {}
class Increment extends CounterEvent {}

// BLoC
class CounterBloc extends Bloc<CounterEvent, int> {
  CounterBloc() : super(0) {
    on<Increment>((event, emit) => emit(state + 1));
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (_) => CounterBloc(),
      child: MaterialApp(home: CounterScreen()),
    );
  }
}

class CounterScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: Text('Bloc Counter')),
      body: Center(
        child: BlocBuilder<CounterBloc, int>(
          builder: (context, count) {
            return Text('Count: $count', style: TextStyle(fontSize: 24));
          },
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: () => context.read<CounterBloc>().add(Increment()),
        child: Icon(Icons.add),
      ),
    );
  }
}

المزايا

  • قابلة للتوسع والصيانة لتطبيقات معقدة
  • فصل واضح بين الطبقات
  • سهلة الاختبار

السلبيات

  • أكثر كودًا مُكررًا
  • منحنى تعليمي أصعب

استخدمها عندما:

  • تبني تطبيقات مؤسسية أو طويلة الأمد
  • تحتاج إلى منطق متوقع وقابل للاختبار
  • يعمل عدة مطورين على وحدات مختلفة من التطبيق

6. GetX — الحل الخفيف المتكامل

GetX هو حل مبسط لكن قوي لإدارة الحالة، ويحتوي أيضًا على التوجيه، والحقول المُستقلة، والأدوات. معروف بكونه أقل كودًا مُكررًا من بين جميع الحلول.

الإعداد

أضف هذا إلى ملف pubspec.yaml:

dependencies:
  get: ^4.6.5

مثال: قائمة تسوق مع GetX

import 'package:flutter/material.dart';
import 'package:get/get.dart';

// المُتحكم
class ShoppingController extends GetxController {
  // قائمة مُراقبة
  var items = <String>[].obs;
  
  // مُراقب عدد العناصر
  var totalItems = 0.obs;

  void addItem(String item) {
    if (item.isNotEmpty) {
      items.add(item);
      totalItems.value = items.length;
    }
  }

  void removeItem(int index) {
    items.removeAt(index);
    totalItems.value = items.length;
  }

  void clearAll() {
    items.clear();
    totalItems.value = 0;
  }
}

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      title: 'GetX Shopping List',
      home: ShoppingScreen(),
    );
  }
}

class ShoppingScreen extends StatelessWidget {
  // تهيئة المُتحكم
  final ShoppingController controller = Get.put(ShoppingController());
  final TextEditingController textController = TextEditingController();

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('GetX Shopping List'),
        actions: [
          Obx(() => Padding(
                padding: EdgeInsets.all(16),
                child: Center(
                  child: Text(
                    'Items: ${controller.totalItems}',
                    style: TextStyle(fontSize: 18),
                  ),
                ),
              )),
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: EdgeInsets.all(16),
            child: Row(
              children: [
                Expanded(
                  child: TextField(
                    controller: textController,
                    decoration: InputDecoration(
                      hintText: 'Enter item name',
                      border: OutlineInputBorder(),
                    ),
                  ),
                ),
                SizedBox(width: 10),
                ElevatedButton(
                  onPressed: () {
                    controller.addItem(textController.text);
                    textController.clear();
                  },
                  child: Icon(Icons.add),
                ),
              ],
            ),
          ),
          Expanded(
            child: Obx(() {
              if (controller.items.isEmpty) {
                return Center(
                  child: Text('No items in your list'),
                );
              }
              
              return ListView.builder(
                itemCount: controller.items.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    leading: CircleAvatar(child: Text('${index + 1}')),
                    title: Text(controller.items[index]),
                    trailing: IconButton(
                      icon: Icon(Icons.delete, color: Colors.red),
                      onPressed: () => controller.removeItem(index),
                    ),
                  );
                },
              );
            }),
          ),
          if (controller.items.isNotEmpty)
            Padding(
              padding: EdgeInsets.all(16),
              child: ElevatedButton(
                onPressed: () {
                  Get.defaultDialog(
                    title: 'Clear List',
                    middleText: 'Are you sure you want to clear all items?',
                    textConfirm: 'Yes',
                    textCancel: 'No',
                    onConfirm: () {
                      controller.clearAll();
                      Get.back();
                    },
                  );
                },
                style: ElevatedButton.styleFrom(
                  backgroundColor: Colors.red,
                  minimumSize: Size(double.infinity, 50),
                ),
                child: Text('Clear All'),
              ),
            ),
        ],
      ),
    );
  }
}

المزايا

  • أقل كودًا مُكررًا — أسرع تطوير
  • حل متكامل (إدارة الحالة، التوجيه، الحقول المُستقلة، الرسائل، النوافذ)
  • لا تحتاج إلى BuildContext
  • خفيف الوزن وسريع
  • سهل التعلم

السلبيات

  • أقل “Flutter-like” مقارنة بـ Provider/BLoC
  • يمكن أن يؤدي إلى ارتباط وثيق إذا لم تكن حذرًا
  • نظام أصغر من Provider
  • بعض المطورين يجدونه “مُسحورًا” جدًا

استخدمها عندما:

  • تطوير نموذج أولي أو MVP
  • تطبيقات صغيرة إلى متوسطة
  • ترغب في أقل كود مُكرر
  • الفريق يفضل البساطة على العمارة الصارمة

اختيار الحل المناسب لإدارة الحالة

الحل التعقيد الكود المُكرر منحنى التعلم الأفضل له
🧱 setState() منخفض قليل سهل حالة محلية بسيطة، نماذج أولية
🏗️ InheritedWidget متوسط مرتفع متوسط تعلم تفاصيل Flutter الداخلية، الحلول المخصصة
🪄 Provider منخفض إلى متوسط منخفض سهل معظم التطبيقات، حالة مشتركة
🔒 Riverpod متوسط منخفض إلى متوسط متوسط التطبيقات الحديثة، الأمان من حيث النوع
📦 BLoC مرتفع مرتفع صعب التطبيقات المؤسسية، المنطق التجاري المعقد
GetX منخفض قليل سهل التطوير السريع، نماذج أولية

دليل قصير لاتخاذ القرار:

تعقيد التطبيق الحل المقترح حالة الاستخدام
🪶 بسيط setState() أو GetX تبديل الأزرار، الرسوم المتحركة، الـwidgets الصغيرة
⚖️ متوسط Provider أو Riverpod المواضيع المشتركة، إعدادات المستخدم، تخزين البيانات
🏗️ معقد BLoC أو Riverpod تطبيقات التجارة الإلكترونية، تطبيقات المحادثة، لوحات التحكم المالية

الخلاصة النهائية

لا يوجد حل واحد يناسب الجميع لإدارة الحالة في Flutter.

هنا نقدم نهجًا عمليًا:

  • ابدأ ببساطة مع setState() للـwidgets البسيطة والنماذج الأولية
  • تعلم الأساسيات مع InheritedWidget لفهم كيفية عمل Flutter داخليًا
  • تدرج إلى Provider عندما ينمو التطبيق ويحتاج إلى حالة مشتركة
  • فكر في Riverpod للمشاريع الجديدة حيث يهم الأمان من حيث النوع والأنماط الحديثة
  • تبنى BLoC للتطبيقات المؤسسية ذات المنطق التجاري المعقد
  • جرب GetX عندما يكون التطوير السريع وقلة الكود المُكرر أولوياتك

الخلاصة الرئيسية:

✅ لا تبالغ في التصميم — استخدم setState() حتى تحتاج إلى أكثر
✅ Provider و Riverpod هما خياران ممتازان لمعظم التطبيقات
✅ BLoC يبرز في الفرق الكبيرة والتطبيقات المعقدة
✅ GetX ممتاز للسرعة، لكن كن حذرًا من الارتباط الوثيق
✅ فهم InheritedWidget يساعدك على إتقان أي حل

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

روابط مكتبات إدارة الحالة في Flutter

روابط مفيدة أخرى