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

FutureProvider و StreamProvider

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

الحالة غير المتزامنة مع Provider

العديد من السيناريوهات في العالم الحقيقي تتضمن بيانات غير متاحة فورًا: جلب ملفات المستخدمين من API، قراءة الإعدادات من التخزين المحلي، أو الاستماع لتحديثات قاعدة البيانات في الوقت الفعلي. يوفر Provider مزودين متخصصين لهذه الأنماط غير المتزامنة: FutureProvider لعمليات التحميل لمرة واحدة وStreamProvider للبيانات التفاعلية المستمرة.

المفهوم الأساسي: كل من FutureProvider وStreamProvider يتعاملان تلقائيًا مع الحالات الثلاث للبيانات غير المتزامنة: التحميل (انتظار البيانات)، البيانات (تم استلام القيمة)، والخطأ (حدث خطأ ما). تتلقى الودجات الحالة المناسبة ويمكنها العرض وفقًا لذلك.

FutureProvider: التهيئة غير المتزامنة لمرة واحدة

FutureProvider يغلف Future ويوفر قيمته المحلولة لشجرة الودجات. إنه مثالي للبيانات التي تحتاج للجلب مرة واحدة عند تحميل شاشة أو تطبيق، مثل إعدادات المستخدم أو الإعدادات عن بُعد أو بيانات API الأولية.

FutureProvider الأساسي

// خدمة تجلب البيانات بشكل غير متزامن
class ConfigService {
  Future<AppConfig> loadConfig() async {
    // محاكاة طلب شبكة
    await Future.delayed(const Duration(seconds: 2));
    return AppConfig(
      apiBaseUrl: 'https://api.example.com',
      maxRetries: 3,
      cacheTimeout: const Duration(minutes: 5),
    );
  }
}

// توفير نتيجة Future لشجرة الودجات
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return FutureProvider<AppConfig?>(
      create: (_) => ConfigService().loadConfig(),
      initialData: null, // القيمة أثناء تحميل المستقبل
      catchError: (context, error) => null, // معالجة الأخطاء
      child: const MaterialApp(home: HomePage()),
    );
  }
}

// استهلاك القيمة المُقدمة
class HomePage extends StatelessWidget {
  const HomePage({super.key});

  @override
  Widget build(BuildContext context) {
    final config = context.watch<AppConfig?>();

    if (config == null) {
      return const Scaffold(
        body: Center(child: CircularProgressIndicator()),
      );
    }

    return Scaffold(
      appBar: AppBar(title: const Text('الرئيسية')),
      body: Center(
        child: Text('رابط API: \${config.apiBaseUrl}'),
      ),
    );
  }
}

عندما يُبنى شجرة الودجات لأول مرة، سيكون config هو null (البيانات الأولية). بمجرد اكتمال Future، يُخطر Provider تلقائيًا جميع المستمعين، وتُعاد بناء الودجت مع كائن AppConfig المحمّل.

تحذير: FutureProvider يستدعي دالة create مرة واحدة فقط. إذا كنت بحاجة لإعادة جلب البيانات (مثلاً السحب للتحديث)، FutureProvider ليس الخيار الصحيح. استخدم ChangeNotifier مع طريقة جلب يدوية بدلاً من ذلك، أو اجمع FutureProvider مع ProxyProvider لتفعيل إعادة الإنشاء عند تغيير الاعتمادات.

FutureProvider مع معالجة الأخطاء

معالجة الأخطاء الصحيحة ضرورية للمزودات غير المتزامنة. معامل catchError يتيح لك توفير قيمة احتياطية عند فشل المستقبل:

معالجة أخطاء FutureProvider

class UserService {
  Future<User> fetchCurrentUser() async {
    final response = await http.get(
      Uri.parse('https://api.example.com/me'),
    );
    if (response.statusCode != 200) {
      throw HttpException('فشل تحميل المستخدم: \${response.statusCode}');
    }
    return User.fromJson(jsonDecode(response.body));
  }
}

// مع معالجة أخطاء صريحة
FutureProvider<User?>(
  create: (_) => UserService().fetchCurrentUser(),
  initialData: null,
  catchError: (context, error) {
    // تسجيل الخطأ
    debugPrint('خطأ في تحميل المستخدم: \$error');
    // إرجاع قيمة احتياطية
    return null;
  },
  child: const MyApp(),
)

// المستهلك يتحقق من null لمعالجة التحميل/الخطأ
class UserGreeting extends StatelessWidget {
  const UserGreeting({super.key});

  @override
  Widget build(BuildContext context) {
    final user = context.watch<User?>();

    if (user == null) {
      return const Text('جاري تحميل المستخدم...');
    }

    return Text('مرحبًا بعودتك، \${user.name}!');
  }
}

StreamProvider: البيانات التفاعلية في الوقت الفعلي

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

StreamProvider الأساسي

// تدفق يُصدر الوقت الحالي كل ثانية
Stream<DateTime> clockStream() {
  return Stream.periodic(
    const Duration(seconds: 1),
    (_) => DateTime.now(),
  );
}

// توفير التدفق لشجرة الودجات
class ClockApp extends StatelessWidget {
  const ClockApp({super.key});

  @override
  Widget build(BuildContext context) {
    return StreamProvider<DateTime>(
      create: (_) => clockStream(),
      initialData: DateTime.now(),
      child: const MaterialApp(home: ClockPage()),
    );
  }
}

// الودجت تتحدث تلقائيًا كل ثانية
class ClockPage extends StatelessWidget {
  const ClockPage({super.key});

  @override
  Widget build(BuildContext context) {
    final now = context.watch<DateTime>();

    return Scaffold(
      body: Center(
        child: Text(
          '\${now.hour.toString().padLeft(2, '0')}'
          ':\${now.minute.toString().padLeft(2, '0')}'
          ':\${now.second.toString().padLeft(2, '0')}',
          style: const TextStyle(fontSize: 48),
        ),
      ),
    );
  }
}

StreamProvider مع الرسائل في الوقت الفعلي

حالة استخدام شائعة لـ StreamProvider هي الرسائل في الوقت الفعلي. إليك مثال عملي يستمع لتدفق محادثة:

محادثة في الوقت الفعلي مع StreamProvider

class ChatService {
  // يُرجع تدفق رسائل من WebSocket أو Firestore
  Stream<List<Message>> getMessages(String chatRoomId) {
    // مثال: مستمع Firestore في الوقت الفعلي
    return FirebaseFirestore.instance
        .collection('chatRooms')
        .doc(chatRoomId)
        .collection('messages')
        .orderBy('timestamp', descending: true)
        .limit(50)
        .snapshots()
        .map((snapshot) => snapshot.docs
            .map((doc) => Message.fromFirestore(doc))
            .toList());
  }
}

// توفير تدفق الرسائل
class ChatRoomPage extends StatelessWidget {
  final String roomId;
  const ChatRoomPage({super.key, required this.roomId});

  @override
  Widget build(BuildContext context) {
    return StreamProvider<List<Message>>(
      create: (_) => ChatService().getMessages(roomId),
      initialData: const [],
      catchError: (_, error) {
        debugPrint('خطأ المحادثة: \$error');
        return const [];
      },
      child: const ChatRoomView(),
    );
  }
}

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

  @override
  Widget build(BuildContext context) {
    final messages = context.watch<List<Message>>();

    return Scaffold(
      appBar: AppBar(title: const Text('المحادثة')),
      body: ListView.builder(
        reverse: true,
        itemCount: messages.length,
        itemBuilder: (context, index) {
          final msg = messages[index];
          return MessageBubble(
            text: msg.text,
            isMe: msg.senderId == currentUserId,
            timestamp: msg.timestamp,
          );
        },
      ),
    );
  }
}

حالات التحميل والخطأ

نهج قوي لمعالجة حالات التحميل والخطأ هو استخدام فئة مغلفة تتتبع صراحةً الحالة غير المتزامنة. بينما لا يتضمن Provider فئة AsyncValue مدمجة، يمكنك إنشاء واحدة بسهولة:

نمط AsyncValue

// مغلف حالة غير متزامن عام
sealed class AsyncValue<T> {
  const AsyncValue();
}

class AsyncLoading<T> extends AsyncValue<T> {
  const AsyncLoading();
}

class AsyncData<T> extends AsyncValue<T> {
  final T value;
  const AsyncData(this.value);
}

class AsyncError<T> extends AsyncValue<T> {
  final Object error;
  final StackTrace? stackTrace;
  const AsyncError(this.error, [this.stackTrace]);
}

// ChangeNotifier يستخدم AsyncValue
class UserProfileNotifier extends ChangeNotifier {
  final ApiService _api;
  AsyncValue<UserProfile> _state = const AsyncLoading();

  UserProfileNotifier({required ApiService api}) : _api = api;

  AsyncValue<UserProfile> get state => _state;

  Future<void> loadProfile() async {
    _state = const AsyncLoading();
    notifyListeners();

    try {
      final data = await _api.get('profile');
      _state = AsyncData(UserProfile.fromJson(data));
    } catch (e, st) {
      _state = AsyncError(e, st);
    }

    notifyListeners();
  }

  Future<void> refresh() => loadProfile();
}

// ودجت تتعامل مع جميع الحالات الثلاث
class ProfilePage extends StatelessWidget {
  const ProfilePage({super.key});

  @override
  Widget build(BuildContext context) {
    final state = context.select<UserProfileNotifier, AsyncValue<UserProfile>>(
      (n) => n.state,
    );

    return switch (state) {
      AsyncLoading() => const Center(
          child: CircularProgressIndicator(),
        ),
      AsyncData(:final value) => ProfileView(profile: value),
      AsyncError(:final error) => Center(
          child: Column(
            mainAxisSize: MainAxisSize.min,
            children: [
              Text('خطأ: \$error'),
              ElevatedButton(
                onPressed: () =>
                    context.read<UserProfileNotifier>().refresh(),
                child: const Text('إعادة المحاولة'),
              ),
            ],
          ),
        ),
    };
  }
}
نصيحة: نمط الفئة المختومة مع تعبيرات switch الشاملة في Dart 3 يضمن أنك تتعامل مع كل حالة ممكنة. المترجم سيحذرك إذا نسيت التعامل مع حالات التحميل أو البيانات أو الخطأ.

الدمج مع المزودات الأخرى

يمكنك دمج FutureProvider وStreamProvider مع ProxyProvider لإنشاء مزودات غير متزامنة تعتمد على قيم أخرى. عند تغيير الاعتماد، يقوم المزود غير المتزامن تلقائيًا بإعادة إنشاء مستقبله أو تدفقه.

StreamProvider مع الاعتمادات

MultiProvider(
  providers: [
    // مزود المصادقة
    ChangeNotifierProvider<AuthModel>(
      create: (_) => AuthModel(),
    ),

    // خدمة قاعدة البيانات تعتمد على المصادقة
    ProxyProvider<AuthModel, DatabaseService>(
      update: (_, auth, __) => DatabaseService(userId: auth.userId),
    ),

    // تدفق طلبات المستخدم — يُعاد الاشتراك عند تغيير DatabaseService
    StreamProvider<List<Order>>(
      create: (context) =>
          context.read<DatabaseService>().watchOrders(),
      initialData: const [],
      catchError: (_, __) => const [],
    ),

    // تحميل الإعدادات عن بُعد — يُحمّل مرة واحدة عند بدء التشغيل
    FutureProvider<RemoteConfig?>(
      create: (_) => RemoteConfigService().fetchConfig(),
      initialData: null,
      catchError: (_, __) => null,
    ),
  ],
  child: const MyApp(),
)

مثال عملي: لوحة تحكم المستخدم مع بيانات غير متزامنة

لنبني لوحة تحكم كاملة تحمّل بيانات المستخدم بشكل غير متزامن وتبث الإشعارات في الوقت الفعلي وتتعامل مع جميع حالات التحميل والخطأ:

لوحة التحكم غير المتزامنة الكاملة

// الخدمات
class NotificationService {
  Stream<List<AppNotification>> watchNotifications(String userId) {
    return FirebaseFirestore.instance
        .collection('notifications')
        .where('userId', isEqualTo: userId)
        .where('read', isEqualTo: false)
        .orderBy('createdAt', descending: true)
        .snapshots()
        .map((snap) => snap.docs
            .map((d) => AppNotification.fromFirestore(d))
            .toList());
  }
}

class SettingsService {
  Future<UserSettings> loadSettings() async {
    final prefs = await SharedPreferences.getInstance();
    return UserSettings(
      darkMode: prefs.getBool('darkMode') ?? false,
      language: prefs.getString('language') ?? 'en',
      notifications: prefs.getBool('notifications') ?? true,
    );
  }
}

// إعداد التطبيق مع مزودات غير متزامنة متعددة
class DashboardApp extends StatelessWidget {
  const DashboardApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        // المصادقة (فورية)
        ChangeNotifierProvider<AuthModel>(
          create: (_) => AuthModel(),
          lazy: false,
        ),

        // الإعدادات (مستقبل — يُحمّل مرة واحدة)
        FutureProvider<UserSettings?>(
          create: (_) => SettingsService().loadSettings(),
          initialData: null,
          catchError: (_, __) => null,
        ),

        // الإشعارات (تدفق — وقت فعلي)
        StreamProvider<List<AppNotification>>(
          create: (context) {
            final auth = context.read<AuthModel>();
            if (auth.userId == null) return const Stream.empty();
            return NotificationService()
                .watchNotifications(auth.userId!);
          },
          initialData: const [],
          catchError: (_, __) => const [],
        ),
      ],
      child: const MaterialApp(home: DashboardPage()),
    );
  }
}

// لوحة التحكم تستهلك جميع المزودات
class DashboardPage extends StatelessWidget {
  const DashboardPage({super.key});

  @override
  Widget build(BuildContext context) {
    final auth = context.watch<AuthModel>();
    final settings = context.watch<UserSettings?>();
    final notifications = context.watch<List<AppNotification>>();

    if (settings == null) {
      return const Scaffold(
        body: Center(child: CircularProgressIndicator()),
      );
    }

    return Scaffold(
      appBar: AppBar(
        title: Text('مرحبًا، \${auth.currentUser?.name ?? "مستخدم"}'),
        actions: [
          Badge(
            label: Text('\${notifications.length}'),
            isLabelVisible: notifications.isNotEmpty,
            child: IconButton(
              icon: const Icon(Icons.notifications),
              onPressed: () => Navigator.pushNamed(
                context, '/notifications',
              ),
            ),
          ),
        ],
      ),
      body: Column(
        children: [
          // الإعدادات المحملة من المستقبل
          Text('السمة: \${settings.darkMode ? "داكنة" : "فاتحة"}'),
          Text('اللغة: \${settings.language}'),
          const Divider(),
          // إشعارات الوقت الفعلي من التدفق
          Expanded(
            child: ListView.builder(
              itemCount: notifications.length,
              itemBuilder: (context, index) {
                final n = notifications[index];
                return ListTile(
                  leading: const Icon(Icons.notification_important),
                  title: Text(n.title),
                  subtitle: Text(n.message),
                );
              },
            ),
          ),
        ],
      ),
    );
  }
}
ملخص: استخدم FutureProvider عندما تحتاج لتحميل البيانات مرة واحدة (الإعدادات، إعدادات المستخدم، استدعاء API الأولي). استخدم StreamProvider عندما تحتاج لتحديثات مستمرة في الوقت الفعلي (رسائل المحادثة، الإشعارات، الأسعار المباشرة). للحالات غير المتزامنة المعقدة التي تتطلب إعادة الجلب أو إمكانية التحديث أو التقسيم إلى صفحات، استخدم ChangeNotifier مع نمط AsyncValue بدلاً من ذلك.