حقن التبعيات: المفاهيم وإعداد GetIt
حقن التبعيات: المفاهيم وإعداد GetIt
مع توسّع تطبيقات Flutter، تتراكم لدى الفئات التبعيات — أي الكائنات الأخرى التي تعتمد عليها لإنجاز عملها. دون نهج مبني على مبادئ واضحة، ينتهي بك الأمر ببناء تلك الكائنات في عمق منطق الأعمال، مما يجعل الكود مستحيل الاختبار وصعب الاستبدال. يحلّ حقن التبعيات (DI) هذه المشكلة بعكس التحكم: بدلاً من أن تُنشئ الفئة تبعياتها بنفسها، تُمرَّر إليها من الخارج. يُرسّخ هذا الدرس المبدأ النظري، ثم يربط بنية Clean Architecture الكاملة باستخدام مُحدِّد الخدمات GetIt.
مبدأ عكس التبعية (DIP)
ينص المبدأ الخامس في SOLID على:
- يجب ألا تعتمد الوحدات عالية المستوى (حالات الاستخدام، الـ Blocs) على الوحدات منخفضة المستوى (عملاء HTTP، قواعد البيانات).
- يجب أن تعتمد كلتا الفئتين على التجريدات (الفئات المجردة / الواجهات).
- يجب ألا تعتمد التجريدات على التفاصيل — بل التفاصيل تعتمد على التجريدات.
عملياً يعني هذا أن GetWeatherUseCase تشير إلى واجهة WeatherRepository، لا إلى التنفيذ الفعلي WeatherRepositoryImpl الذي يستدعي واجهة برمجية. يُحقَن التنفيذ الفعلي عند نقطة التركيب.
أنواع تسجيلات GetIt
يوفر GetIt ثلاث استراتيجيات للتسجيل، لكلٍّ منها دلالات مختلفة لعمر الكائن:
registerSingleton<T>(T instance)— يُنشئ الكائن فوراً؛ وتُعاد نفس النسخة في كل استدعاءget<T>().registerLazySingleton<T>(() => T)— يُؤجّل الإنشاء حتى أول استدعاء لـget<T>()؛ وتُعاد نفس النسخة بعد ذلك. الأفضل للتبعيات الثقيلة التي قد لا تُستخدم دائماً.registerFactory<T>(() => T)— يُنشئ نسخة جديدة في كل استدعاء لـget<T>(). مثالي لـ Blocs/Cubits حتى تحصل كل شاشة على آلة حالة نظيفة.
إضافة 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()، هو كل ما تحتاجه لربط رسم بياني كامل للتبعيات.