بنية التطبيق وأنماط التصميم

حقن التبعيات: المفاهيم وإعداد GetIt

16 دقيقة الدرس 8 من 12

حقن التبعيات: المفاهيم وإعداد GetIt

مع توسّع تطبيقات Flutter، تتراكم لدى الفئات التبعيات — أي الكائنات الأخرى التي تعتمد عليها لإنجاز عملها. دون نهج مبني على مبادئ واضحة، ينتهي بك الأمر ببناء تلك الكائنات في عمق منطق الأعمال، مما يجعل الكود مستحيل الاختبار وصعب الاستبدال. يحلّ حقن التبعيات (DI) هذه المشكلة بعكس التحكم: بدلاً من أن تُنشئ الفئة تبعياتها بنفسها، تُمرَّر إليها من الخارج. يُرسّخ هذا الدرس المبدأ النظري، ثم يربط بنية Clean Architecture الكاملة باستخدام مُحدِّد الخدمات GetIt.

مبدأ عكس التبعية (DIP)

ينص المبدأ الخامس في SOLID على:

  • يجب ألا تعتمد الوحدات عالية المستوى (حالات الاستخدام، الـ Blocs) على الوحدات منخفضة المستوى (عملاء HTTP، قواعد البيانات).
  • يجب أن تعتمد كلتا الفئتين على التجريدات (الفئات المجردة / الواجهات).
  • يجب ألا تعتمد التجريدات على التفاصيل — بل التفاصيل تعتمد على التجريدات.

عملياً يعني هذا أن GetWeatherUseCase تشير إلى واجهة WeatherRepository، لا إلى التنفيذ الفعلي WeatherRepositoryImpl الذي يستدعي واجهة برمجية. يُحقَن التنفيذ الفعلي عند نقطة التركيب.

ملاحظة: حقن التبعيات هو الأسلوب؛ أما مبدأ عكس التبعية فهو السبب الذي يدفعك لتطبيقه. يمكنك الحقن اليدوي (تمرير الكائنات عبر المُنشئ) أو استخدام مكتبة. GetIt مكتبة محدِّد خدمات — خيار شائع وخفيف الوزن لـ Flutter لا يتطلب توليد كود.

أنواع تسجيلات GetIt

يوفر GetIt ثلاث استراتيجيات للتسجيل، لكلٍّ منها دلالات مختلفة لعمر الكائن:

  • registerSingleton<T>(T instance) — يُنشئ الكائن فوراً؛ وتُعاد نفس النسخة في كل استدعاء get<T>().
  • registerLazySingleton<T>(() => T) — يُؤجّل الإنشاء حتى أول استدعاء لـ get<T>()؛ وتُعاد نفس النسخة بعد ذلك. الأفضل للتبعيات الثقيلة التي قد لا تُستخدم دائماً.
  • registerFactory<T>(() => T) — يُنشئ نسخة جديدة في كل استدعاء لـ get<T>(). مثالي لـ Blocs/Cubits حتى تحصل كل شاشة على آلة حالة نظيفة.
نصيحة: سجِّل البنية التحتية منخفضة المستوى (عميل HTTP، قاعدة البيانات) كـ Singletons أو Lazy Singletons. وسجِّل ViewModels وBlocs وCubits كـ Factories حتى تبدأ كل مسار بحالة نظيفة.

إضافة GetIt إلى مشروعك

أضف الحزمة إلى pubspec.yaml، ثم نفِّذ flutter pub get:

اعتماد pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  get_it: ^7.7.0          # محدِّد الخدمات
  dio: ^5.4.0             # عميل HTTP (مثال على تبعية بنية تحتية)
  shared_preferences: ^2.2.3

بناء حاوية الحقن

الاتفاقية في مشاريع Flutter ذات بنية Clean Architecture هي ملف واحد — عادةً lib/core/di/injection_container.dart — يسجّل كل تبعية بالترتيب الصحيح (البنية التحتية أولاً، ثم البيانات، ثم النطاق، ثم العرض).

lib/core/di/injection_container.dart

import 'package:get_it/get_it.dart';
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';

// التجريدات في طبقة النطاق
import '../../features/weather/domain/repositories/weather_repository.dart';
import '../../features/weather/domain/usecases/get_current_weather.dart';

// التنفيذات في طبقة البيانات
import '../../features/weather/data/datasources/weather_remote_datasource.dart';
import '../../features/weather/data/repositories/weather_repository_impl.dart';

// طبقة العرض
import '../../features/weather/presentation/bloc/weather_bloc.dart';

/// نسخة مُحدِّد الخدمات العامة — مُتاحة لكامل التطبيق.
final GetIt sl = GetIt.instance;

/// استدعِها مرة واحدة في [main] قبل [runApp].
Future<void> initDependencies() async {
  // ── 1. خارجي / طرف ثالث ──────────────────────────────────────────
  // SharedPreferences تتطلب انتظاراً قبل التسجيل.
  final sharedPrefs = await SharedPreferences.getInstance();
  sl.registerSingleton<SharedPreferences>(sharedPrefs);

  // Dio Singleton — مشترك بين جميع استدعاءات الشبكة.
  sl.registerLazySingleton<Dio>(
    () => Dio(BaseOptions(
      baseUrl: 'https://api.openweathermap.org/data/2.5/',
      connectTimeout: const Duration(seconds: 10),
      receiveTimeout: const Duration(seconds: 15),
    )),
  );

  // ── 2. طبقة البيانات ──────────────────────────────────────────────
  sl.registerLazySingleton<WeatherRemoteDataSource>(
    () => WeatherRemoteDataSourceImpl(dio: sl()),
  );

  // تسجيل التنفيذ تحت النوع المجرد حتى يشير المستدعون للواجهة.
  sl.registerLazySingleton<WeatherRepository>(
    () => WeatherRepositoryImpl(remoteDataSource: sl()),
  );

  // ── 3. طبقة النطاق ────────────────────────────────────────────────
  sl.registerLazySingleton(
    () => GetCurrentWeather(repository: sl()),
  );

  // ── 4. طبقة العرض ─────────────────────────────────────────────────
  // Factory: كل شاشة تحصل على نسخة WeatherBloc جديدة.
  sl.registerFactory(
    () => WeatherBloc(getCurrentWeather: sl()),
  );
}

التشغيل الأولي في main.dart

انتظر initDependencies() قبل استدعاء runApp حتى يكون كل Singleton جاهزاً قبل تركيب شجرة الودجات:

lib/main.dart

import 'package:flutter/material.dart';
import 'core/di/injection_container.dart' as di;
import 'app.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();
  await di.initDependencies();   // سجِّل كل الخدمات قبل runApp
  runApp(const App());
}

حل التبعيات في الودجات

استدعِ sl<T>() (أو ما يعادله GetIt.instance<T>()) في أي مكان تحتاج فيه إلى نسخة. الاستخدام الأكثر شيوعاً هو داخل BlocProvider:

حل Bloc مسجَّل كـ Factory

import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../../core/di/injection_container.dart';
import '../bloc/weather_bloc.dart';
import 'weather_view.dart';

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

  @override
  Widget build(BuildContext context) {
    // sl<WeatherBloc>() يستدعي الـ Factory → bloc جديد في كل مرة
    return BlocProvider(
      create: (_) => sl<WeatherBloc>(),
      child: const WeatherView(),
    );
  }
}
تحذير: لا تستدعِ sl<T>() داخل دالة build() مباشرةً دون تغليفها في BlocProvider أو ما شابهه. استدعاء تسجيل Factory مباشرةً في build() يُنشئ نسخة جديدة في كل إعادة بناء، مما يُسبّب تسرب ذاكرة وفقدان للحالة.

لماذا GetIt بدلاً من الحقن عبر المُنشئ فقط؟

الحقن عبر المُنشئ هو أنقى أشكال DI ويُفضَّل في طبقات النطاق والبيانات. يتألق GetIt عند جذر التركيب — المكان الوحيد الذي يربط كل شيء معاً. من المزايا:

  • لا يتطلب توليد كود (على عكس injectable أو auto_route).
  • يعمل خارج شجرة الودجات — استخدم sl() في فئات Dart عادية.
  • يدعم التسجيل غير المتزامن (registerSingletonAsync) للتهيئات التي تتطلب انتظاراً.
  • سهل إعادة التعيين في الاختبارات: sl.reset() يمسح جميع التسجيلات للحصول على صفحة نظيفة.

ملخص

يُفكّك حقن التبعيات — بتوجيه من مبدأ عكس التبعية — طبقاتك ويجعل كل مكوّن قابلاً للاختبار باستقلالية. تتماشى أوضاع تسجيل GetIt الثلاثة — Singleton وLazy Singleton وFactory — بشكل مثالي مع أعمار Clean Architecture: البنية التحتية تعيش طوال عمر التطبيق، بينما تُعاد إنشاء الـ Blocs مع كل شاشة. ملف injection_container.dart واحد، مُهيَّأ في main()، هو كل ما تحتاجه لربط رسم بياني كامل للتبعيات.