إدارة الحالة المتقدمة (Bloc و Riverpod)

حقن التبعيات مع Riverpod

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

حقن التبعيات مع Riverpod

حقن التبعيات (DI) هو ممارسة توفير المتعاونين مع الكائن من الخارج بدلاً من السماح له بإنشائهم بنفسه. في تطبيقات Flutter المبنية باستخدام Riverpod، المزودات (providers) هي آلية الحقن: يُعرَّف كل مستودع وعميل HTTP وخدمة كمزوّد مستقل، وتكتفي المزودات (notifiers) ذات المستوى الأعلى بإدراج المزودات التي تعتمد عليها. يُنتج ذلك بنية نظيفة تكون فيها كل طبقة قابلة للاختبار المستقل والتبديل دون المساس بالودجات.

لماذا تهم التبعيات الصريحة

حين يُنشئ StateNotifier أو AsyncNotifier عميل http.Client أو ApiService داخلياً، لا يمكن استبدال تلك التبعية في الاختبار دون تعديل الحالة العامة. في المقابل، حين تُعلَن التبعية كمزوّد وتُقرأ عبر ref، يستطيع الاختبار تجاوز أي مزوّد بنسخة وهمية في سطر واحد.

  • الفصل: منطق الأعمال لا يرتبط بتنفيذ محدد.
  • قابلية الاختبار: استبدل الخدمات الحقيقية بنسخ وهمية عبر ProviderContainer.overrides.
  • قابلية التركيب: يمكن للمزودات أن تعتمد على مزودات أخرى في مخطط دوري غير حلقي.
  • النطاق: يُنهي AutoDisposeProvider الموارد تلقائياً حين لا يراقبه أي ودجت.

ربط عميل HTTP بمستودع

يتكون النمط من ثلاث طبقات. أولاً، اعرض البنية التحتية على المستوى المنخفض (مثل http.Client). ثانياً، اعرض المستودع الذي يستخدمها. ثالثاً، اعرض المزوّد الذي يستخدم المستودع. تقرأ كل طبقة الطبقة التي تقع أدناها عبر ref.watch أو ref.read.

سلسلة المزودات ذات الثلاث طبقات

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http;

// --- الطبقة 1: البنية التحتية ---
final httpClientProvider = Provider<http.Client>((ref) {
  final client = http.Client();
  // إغلاق العميل تلقائياً عند التخلص من المزوّد
  ref.onDispose(client.close);
  return client;
});

// --- الطبقة 2: المستودع ---
class PostRepository {
  final http.Client _client;
  PostRepository(this._client);

  Future<List<Post>> fetchPosts() async {
    final response = await _client.get(
      Uri.parse('https://jsonplaceholder.typicode.com/posts'),
    );
    if (response.statusCode != 200) {
      throw Exception('فشل تحميل المنشورات');
    }
    return parsePostsJson(response.body);
  }
}

final postRepositoryProvider = Provider<PostRepository>((ref) {
  // المستودع يُعلن تبعيته على عميل HTTP
  final client = ref.watch(httpClientProvider);
  return PostRepository(client);
});

// --- الطبقة 3: المزوّد غير المتزامن ---
final postsNotifierProvider =
    AsyncNotifierProvider<PostsNotifier, List<Post>>(PostsNotifier.new);

class PostsNotifier extends AsyncNotifier<List<Post>> {
  @override
  Future<List<Post>> build() {
    // المزوّد يُعلن تبعيته على المستودع
    return ref.watch(postRepositoryProvider).fetchPosts();
  }
}
ملاحظة: ref.watch داخل دالة build للمزوّد يُنشئ تبعية تفاعلية. لو تم تجاوز httpClientProvider (مثلاً في اختبار)، فسيُعاد بناء postRepositoryProvider تلقائياً بالعميل الجديد، ومن ثَمَّ سيُعاد بناء postsNotifierProvider أيضاً.

تجاوز المزودات في الاختبارات

الهدف كله من حقن التبعيات عبر Riverpod هو إمكانية استبدال أي مزوّد بنسخة وهمية على حدود الاختبار دون تعديل كود الإنتاج. استخدم ProviderContainer مع معامل overrides لحقن النسخ الوهمية.

حقن مستودع وهمي في اختبار وحدة

import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

class FakePostRepository implements PostRepository {
  FakePostRepository(http.Client _);  // متجاهل — الاختبار لا يستخدم HTTP حقيقياً

  @override
  Future<List<Post>> fetchPosts() async => [
    Post(id: 1, title: 'منشور تجريبي'),
  ];
}

void main() {
  test('يحمل PostsNotifier المنشورات من المستودع', () async {
    final container = ProviderContainer(
      overrides: [
        // استبدال المستودع الحقيقي بالنسخة الوهمية
        postRepositoryProvider.overrideWithValue(
          FakePostRepository(http.Client()),
        ),
      ],
    );
    addTearDown(container.dispose);

    // قراءة المزوّد والانتظار حتى اكتمال البناء غير المتزامن
    final state = await container
        .read(postsNotifierProvider.future);

    expect(state.length, 1);
    expect(state.first.title, 'منشور تجريبي');
  });
}
نصيحة: تحتاج فقط لتجاوز الطبقة التي تريد محاكاتها. إذا جاوزت postRepositoryProvider، سيستخدم Riverpod النسخة الوهمية للمزوّد أيضاً — دون المساس بـ httpClientProvider إطلاقاً. جاوز أصغر نطاق ممكن للحفاظ على سرعة الاختبارات وتركيزها.

استخدام مزودات Family للتبعيات ذات المعاملات

حين يجب تحديد نطاق مستودع أو خدمة بقيمة وقت التشغيل (مثل معرّف المستخدم الحالي أو مستأجر محدد)، استخدم المعدِّل .family. يصبح المعامل جزءاً من هوية المزوّد، بحيث يُنشئ كل وسيط فريد نسخة مستقلة مخزنة مؤقتاً.

مزوّد Family لخدمة مُقيَّدة بمستخدم

final userRepositoryProvider =
    Provider.family<UserRepository, String>((ref, userId) {
  final client = ref.watch(httpClientProvider);
  return UserRepository(client: client, userId: userId);
});

// ودجت المستهلك
class UserProfileWidget extends ConsumerWidget {
  final String userId;
  const UserProfileWidget({required this.userId, super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    final repo = ref.watch(userRepositoryProvider(userId));
    // repo هو UserRepository مُقيَّد بهذا userId
    return Text(repo.toString());
  }
}

البنية النظيفة: نظرة سريعة على الطبقات

يرتبط تطبيق Riverpod المُهيكَل جيداً ارتباطاً واضحاً بطبقات البنية النظيفة:

  • طبقة البنية التحتية — عملاء HTTP ومحوّلات قواعد البيانات وواجهات برمجة الجهاز — تُعرَّض كـ Provider بسيط.
  • طبقة البيانات — المستودعات التي تحوّل البيانات الخام إلى نماذج النطاق — أيضاً Provider يقرأ مزودات البنية التحتية.
  • طبقة النطاق — مزودات حالات الاستخدام (AsyncNotifier، Notifier) التي تقرأ مزودات المستودعات.
  • طبقة العرض — ودجات ConsumerWidget وConsumerStatefulWidget التي تراقب مزودات المزودين.
تحذير: لا تُنشئ الخدمات أو المستودعات مباشرةً داخل ConsumerWidget. تُعاد بناء الودجات بشكل متكرر؛ إنشاء http.Client جديد أو فتح اتصال قاعدة بيانات عند كل بناء يُهدر الموارد. فوّض دائماً الكائنات طويلة الأمد للمزودات مع التخلص المناسب (ref.onDispose).

ملخص

مخطط مزودات Riverpod هو حاوية حقن التبعيات المثالية لـ Flutter. بإعلان كل متعاون كمزوّد مستقل وقراءة المستهلكين لها عبر ref، تحقق فصلاً صارماً بين الطبقات، ومحاكاة بلا نمطية مكررة في الاختبارات، وإدارة دورة حياة تلقائية — كل ذلك دون استدعاء مُنشئ واحد يدوياً داخل ودجت.