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

MultiProvider و ProxyProvider

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

إدارة مزودات متعددة

تطبيقات Flutter في العالم الحقيقي نادرًا ما تعتمد على مزود واحد. ستحتاج عادةً إلى حالة المصادقة وبيانات ملف المستخدم وخدمات API وتفضيلات السمة والعديد من أجزاء الحالة الأخرى التي تحتاج أن تكون متاحة في جميع أنحاء شجرة الودجات. يوفر Provider أداتين قويتين لإدارة هذا التعقيد: MultiProvider لتنظيم مزودات متعددة، وProxyProvider لإنشاء مزودات تعتمد على مزودات أخرى.

MultiProvider: تنظيم نظيف

بدون MultiProvider، يؤدي تداخل مزودات متعددة إلى كود عميق المسافات وصعب القراءة. MultiProvider يُسطّح هذا التداخل إلى قائمة نظيفة وقابلة للقراءة.

بدون MultiProvider (متداخل)

// متداخل بعمق — صعب القراءة والصيانة
Widget build(BuildContext context) {
  return ChangeNotifierProvider<AuthModel>(
    create: (_) => AuthModel(),
    child: ChangeNotifierProvider<ThemeModel>(
      create: (_) => ThemeModel(),
      child: ChangeNotifierProvider<CartModel>(
        create: (_) => CartModel(),
        child: Provider<ApiService>(
          create: (_) => ApiService(),
          child: const MyApp(),
        ),
      ),
    ),
  );
}

مع MultiProvider (مسطّح)

// نظيف وقابل للقراءة — نفس النتيجة، هيكل أفضل
Widget build(BuildContext context) {
  return MultiProvider(
    providers: [
      ChangeNotifierProvider<AuthModel>(create: (_) => AuthModel()),
      ChangeNotifierProvider<ThemeModel>(create: (_) => ThemeModel()),
      ChangeNotifierProvider<CartModel>(create: (_) => CartModel()),
      Provider<ApiService>(create: (_) => ApiService()),
    ],
    child: const MyApp(),
  );
}
مهم: ترتيب المزودات في قائمة MultiProvider مهم. يتم إنشاء المزودات من الأعلى إلى الأسفل، ويمكنها فقط الوصول إلى المزودات المعلنة فوقها في القائمة. إذا كان CartModel يعتمد على AuthModel، يجب أن يأتي AuthModel أولاً.

ProxyProvider: المزودات التابعة

ProxyProvider ينشئ مزودًا قيمته تعتمد على مزود آخر. عندما يتغير الاعتماد، يقوم المزود الوسيط تلقائيًا بتحديث قيمته. هذا ضروري لبناء طبقات الخدمة حيث تحتاج خدمة واحدة بيانات من أخرى.

ProxyProvider الأساسي

class AuthModel extends ChangeNotifier {
  String? _token;
  String? get token => _token;
  bool get isLoggedIn => _token != null;

  void login(String token) {
    _token = token;
    notifyListeners();
  }

  void logout() {
    _token = null;
    notifyListeners();
  }
}

class ApiService {
  final String? authToken;

  ApiService({this.authToken});

  Map<String, String> get headers => {
    'Content-Type': 'application/json',
    if (authToken != null) 'Authorization': 'Bearer \$authToken',
  };

  Future<Map<String, dynamic>> fetchData(String endpoint) async {
    // يستخدم authToken في الرؤوس تلقائيًا
    final response = await http.get(
      Uri.parse('https://api.example.com/\$endpoint'),
      headers: headers,
    );
    return jsonDecode(response.body);
  }
}

// الإعداد: ApiService يعتمد على AuthModel
Widget build(BuildContext context) {
  return MultiProvider(
    providers: [
      ChangeNotifierProvider<AuthModel>(
        create: (_) => AuthModel(),
      ),
      // ProxyProvider يعيد بناء ApiService عند تغيير AuthModel
      ProxyProvider<AuthModel, ApiService>(
        update: (context, auth, previousApi) => ApiService(
          authToken: auth.token,
        ),
      ),
    ],
    child: const MyApp(),
  );
}

في هذا المثال، في كل مرة يُخطر فيها AuthModel مستمعيه (مثلاً بعد تسجيل الدخول أو الخروج)، ينشئ ProxyProvider نسخة جديدة من ApiService مع رمز المصادقة المحدث. أي ودجت تراقب ApiService ستتلقى تلقائيًا النسخة الجديدة.

ProxyProvider2 و ProxyProvider3

عندما يعتمد مزود على اثنين أو ثلاثة مزودات أخرى، استخدم ProxyProvider2 أو ProxyProvider3. هذه المتغيرات تقبل معاملات أنواع متعددة وتوفر جميع الاعتمادات في استدعاء update.

ProxyProvider2 — اعتمادان

class UserRepository {
  final ApiService api;
  final CacheService cache;

  UserRepository({required this.api, required this.cache});

  Future<User> getUser(String id) async {
    // تحقق من الذاكرة المؤقتة أولاً
    final cached = cache.get('user_\$id');
    if (cached != null) return User.fromJson(cached);

    // جلب من API إذا لم يكن مُخزّنًا
    final data = await api.fetchData('users/\$id');
    cache.set('user_\$id', data);
    return User.fromJson(data);
  }
}

// الإعداد مع ProxyProvider2
MultiProvider(
  providers: [
    ChangeNotifierProvider<AuthModel>(create: (_) => AuthModel()),
    Provider<CacheService>(create: (_) => CacheService()),
    ProxyProvider<AuthModel, ApiService>(
      update: (_, auth, __) => ApiService(authToken: auth.token),
    ),
    // UserRepository يعتمد على كل من ApiService و CacheService
    ProxyProvider2<ApiService, CacheService, UserRepository>(
      update: (_, api, cache, __) => UserRepository(
        api: api,
        cache: cache,
      ),
    ),
  ],
  child: const MyApp(),
)

ProxyProvider3 — ثلاث اعتمادات

class AnalyticsService {
  final AuthModel auth;
  final ApiService api;
  final AppConfig config;

  AnalyticsService({
    required this.auth,
    required this.api,
    required this.config,
  });

  void trackEvent(String event) {
    if (!config.analyticsEnabled) return;
    api.fetchData('analytics/track?event=\$event&user=\${auth.token}');
  }
}

// ProxyProvider3 مع ثلاث اعتمادات
ProxyProvider3<AuthModel, ApiService, AppConfig, AnalyticsService>(
  update: (_, auth, api, config, __) => AnalyticsService(
    auth: auth,
    api: api,
    config: config,
  ),
)
نصيحة: إذا كنت بحاجة لأكثر من ثلاث اعتمادات، فكر في إعادة هيكلة بنيتك. مزود يعتمد على أربعة مزودات أو أكثر هو علامة على أن الفئة تقوم بالكثير. قسّمها إلى خدمات أصغر ومركزة.

ChangeNotifierProxyProvider

عندما يكون مزودك التابع هو ChangeNotifier (وليس كائنًا عاديًا فقط)، استخدم ChangeNotifierProxyProvider. هذا المتغير يتخلص بشكل صحيح من المُخطِر عندما لم يعد مطلوبًا ويوفر النسخة السابقة للحفاظ على الحالة.

ChangeNotifierProxyProvider

class UserProfileModel extends ChangeNotifier {
  final ApiService _api;
  User? _user;
  bool _isLoading = false;

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

  User? get user => _user;
  bool get isLoading => _isLoading;

  // تحديث خدمة API عند تغيير المصادقة
  void updateApi(ApiService newApi) {
    // إعادة جلب بيانات المستخدم مع بيانات الاعتماد الجديدة
    if (_user != null) {
      loadUser(_user!.id);
    }
  }

  Future<void> loadUser(String id) async {
    _isLoading = true;
    notifyListeners();

    try {
      final data = await _api.fetchData('users/\$id');
      _user = User.fromJson(data);
    } catch (e) {
      _user = null;
    }

    _isLoading = false;
    notifyListeners();
  }
}

// الإعداد: UserProfileModel يعتمد على ApiService وهو ChangeNotifier
MultiProvider(
  providers: [
    ChangeNotifierProvider<AuthModel>(create: (_) => AuthModel()),
    ProxyProvider<AuthModel, ApiService>(
      update: (_, auth, __) => ApiService(authToken: auth.token),
    ),
    ChangeNotifierProxyProvider<ApiService, UserProfileModel>(
      create: (context) => UserProfileModel(
        api: context.read<ApiService>(),
      ),
      update: (context, api, previous) {
        // الحفاظ على الحالة الموجودة، تحديث الاعتماد
        previous!.updateApi(api);
        return previous;
      },
    ),
  ],
  child: const MyApp(),
)
تحذير: في استدعاء update لـ ChangeNotifierProxyProvider، أعد دائمًا نسخة previous عندما يكون ذلك ممكنًا. إنشاء نسخة جديدة في كل مرة سيفقد الحالة المتراكمة (البيانات المحملة، الأعلام، إلخ) ويمكن أن يسبب حلقات إعادة بناء لانهائية.

التحميل الكسول للمزودات

بشكل افتراضي، المزودات في Flutter هي كسولة — لا يتم إنشاؤها حتى تصل إليها ودجت لأول مرة. هذا مفيد للأداء لأن المزودات للميزات التي لم يزرها المستخدم بعد لن تستهلك موارد.

التحكم في التحميل الكسول

MultiProvider(
  providers: [
    // كسول (افتراضي) — يُنشأ عند الوصول الأول
    ChangeNotifierProvider<CartModel>(
      create: (_) => CartModel(),
      // lazy: true هو الافتراضي
    ),

    // فوري — يُنشأ فورًا عند بناء MultiProvider
    ChangeNotifierProvider<AuthModel>(
      create: (_) => AuthModel()..checkStoredSession(),
      lazy: false, // يُنشأ فورًا
    ),

    // فوري — مفيد للمزودات التي تحتاج تهيئة
    Provider<DatabaseService>(
      create: (_) => DatabaseService()..initialize(),
      lazy: false, // بدء اتصال قاعدة البيانات فورًا
      dispose: (_, db) => db.close(), // تنظيف عند التخلص
    ),
  ],
  child: const MyApp(),
)
نصيحة: اضبط lazy: false للمزودات التي تقوم بعمل تهيئة (مثل فحص رموز المصادقة المخزنة أو فتح اتصالات قاعدة البيانات). اترك جميع المزودات الأخرى كسولة لتجنب استخدام الموارد غير الضروري عند بدء التشغيل.

نطاق المزود والتخلص

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

المزودات المحددة النطاق

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

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      // مزودات على مستوى التطبيق — تعيش طوال عمر التطبيق
      providers: [
        ChangeNotifierProvider(create: (_) => AuthModel()),
        ChangeNotifierProvider(create: (_) => ThemeModel()),
      ],
      child: MaterialApp(
        routes: {
          '/': (_) => const HomePage(),
          '/shop': (_) => ChangeNotifierProvider(
            // مزود على مستوى الصفحة — يُتخلص منه عند مغادرة /shop
            create: (_) => CartModel(),
            child: const ShopPage(),
          ),
          '/chat': (_) => ChangeNotifierProvider(
            // مزود على مستوى الصفحة — يُتخلص منه عند مغادرة /chat
            create: (_) => ChatModel(),
            child: const ChatPage(),
          ),
        },
      ),
    );
  }
}

مثال عملي: سلسلة المصادقة + ملف المستخدم

لنبني مثالاً كاملاً من العالم الحقيقي يربط المصادقة وخدمة API ومستودع المستخدم وملف المستخدم معًا:

سلسلة المزودات الكاملة

// 1. المصادقة توفر الرمز
class AuthModel extends ChangeNotifier {
  String? _token;
  User? _currentUser;

  String? get token => _token;
  User? get currentUser => _currentUser;
  bool get isLoggedIn => _token != null;

  Future<void> login(String email, String password) async {
    final response = await http.post(
      Uri.parse('https://api.example.com/login'),
      body: jsonEncode({'email': email, 'password': password}),
    );
    final data = jsonDecode(response.body);
    _token = data['token'];
    _currentUser = User.fromJson(data['user']);
    notifyListeners();
  }

  void logout() {
    _token = null;
    _currentUser = null;
    notifyListeners();
  }
}

// 2. ApiService يعتمد على AuthModel للرمز
class ApiService {
  final String? authToken;
  ApiService({this.authToken});

  Future<dynamic> get(String path) async {
    final response = await http.get(
      Uri.parse('https://api.example.com/\$path'),
      headers: {
        'Content-Type': 'application/json',
        if (authToken != null) 'Authorization': 'Bearer \$authToken',
      },
    );
    return jsonDecode(response.body);
  }
}

// 3. UserRepository يعتمد على ApiService
class UserRepository {
  final ApiService _api;
  UserRepository({required ApiService api}) : _api = api;

  Future<UserProfile> getProfile() async {
    final data = await _api.get('profile');
    return UserProfile.fromJson(data);
  }

  Future<List<Order>> getOrders() async {
    final data = await _api.get('orders');
    return (data as List).map((e) => Order.fromJson(e)).toList();
  }
}

// 4. ربط كل شيء معًا
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        // الطبقة 1: المصادقة (مستقلة)
        ChangeNotifierProvider<AuthModel>(
          create: (_) => AuthModel(),
          lazy: false, // فحص الجلسة المخزنة فورًا
        ),

        // الطبقة 2: خدمة API (تعتمد على المصادقة)
        ProxyProvider<AuthModel, ApiService>(
          update: (_, auth, __) => ApiService(
            authToken: auth.token,
          ),
        ),

        // الطبقة 3: مستودع المستخدم (يعتمد على خدمة API)
        ProxyProvider<ApiService, UserRepository>(
          update: (_, api, __) => UserRepository(api: api),
        ),
      ],
      child: MaterialApp(
        home: Consumer<AuthModel>(
          builder: (context, auth, _) {
            return auth.isLoggedIn
                ? const DashboardPage()
                : const LoginPage();
          },
        ),
      ),
    );
  }
}

// 5. الاستخدام في الودجات
class DashboardPage extends StatelessWidget {
  const DashboardPage({super.key});

  @override
  Widget build(BuildContext context) {
    final user = context.select<AuthModel, User?>(
      (auth) => auth.currentUser,
    );

    return Scaffold(
      appBar: AppBar(
        title: Text('مرحبًا، \${user?.name ?? "مستخدم"}'),
        actions: [
          IconButton(
            icon: const Icon(Icons.logout),
            onPressed: () => context.read<AuthModel>().logout(),
          ),
        ],
      ),
      body: FutureBuilder<UserProfile>(
        future: context.read<UserRepository>().getProfile(),
        builder: (context, snapshot) {
          if (snapshot.connectionState == ConnectionState.waiting) {
            return const Center(child: CircularProgressIndicator());
          }
          if (snapshot.hasError) {
            return Center(child: Text('خطأ: \${snapshot.error}'));
          }
          final profile = snapshot.data!;
          return ProfileCard(profile: profile);
        },
      ),
    );
  }
}
ملاحظة معمارية: لاحظ سلسلة الاعتمادات الطبقية: المصادقة → خدمة API → مستودع المستخدم. عندما يسجل المستخدم الدخول أو الخروج، يتغير رمز المصادقة، مما يُفعّل خدمة API جديدة، مما يُفعّل مستودع مستخدم جديد. السلسلة بأكملها تتحدث تلقائيًا من خلال نظام Provider التفاعلي.