حقن التبعيات (Dependency Injection) Dependency Injection (GetIt)
إدارة العلاقات بين الكائنات وتسهيل الوصول للخدمات باستخدام نظام Service Locator. Managing object relationships and facilitating service access using a Service Locator system.
تخيل إنك بتبني بيت. هل ينفع كل ما تحتاج شاكوش تروح تصنعه من الصفر؟ طبعاً لأ. الأفضل يكون فيه "شنطة عدة" فيها الشاكوش جاهز، وإنت تطلبه وقت ما تحتاجه. ده بالضبط الـ Dependency Injection.
Imagine building a house. Should you manufacture a hammer from scratch every time you need one? Of course not. Better to have a "toolbox" where the hammer is ready, and you request it when needed. This is Dependency Injection.
في البرمجة، الـ Dependency هو أي كلاس كلاس تاني محتاجه عشان يشتغل (زي الـ Repository اللي الـ BLoC محتاجه).
إحنا اختارنا مكتبة GetIt لأنها بسيطة، سريعة، ومش محتاجة BuildContext. يعني تقدر تجيب أي خدمة حتى لو إنت بره الـ Widgets خالص.
// إزاي بنطلب خدمة؟
final repository = sl<IBookRepository>();
في GetIt، عندنا طريقتين أساسيتين لتسجيل الكلاسات:
بيكّريه نسخة واحدة بس من الكلاس، ويفضل محتفظ بيها طول ما الأبلكيشن شغال. بنستخدمه مع الداتابيز والـ Repositories.
بيكريه نسخة "جديدة" في كل مرة تطلبه فيها. بنستخدمه مع الـ Blocs والـ Cubits عشان كل شاشة تاخد نسخة فريش.
تعالوا نشوف إزاي بنرتب "شنطة العدة" بتاعتنا في التطبيق:
final sl = GetIt.instance; // sl = Service Locator
Future initDI() async {
// تسجيل الداتابيز كـ Singleton (نسخة واحدة للكل)
sl.registerLazySingleton(() => AppDatabase());
// تسجيل الـ Repository مع حقن الـ DAO جواه
sl.registerLazySingleton(
() => BookRepositoryImpl(sl()),
);
// تسجيل البلوك كـ Factory (نسخة جديدة لكل شاشة)
sl.registerFactory(() => BooksBloc(repository: sl()));
}
لاحظ هنا إننا بنعمل "حقن تلقائي". الـ Repository محتاج DAO، فإحنا بنقول لـ GetIt: "روحي هاتي الـ DAO اللي سجلناه فوق وحطيه هنا".
أحياناً، الـ Cubit مش بس محتاج Repository، ده محتاج معلومة تانية بتيجي وقت التشغيل (زي الـ bookId). إزاي بنبعت المعلومة دي لـ GetIt؟
// التسجيل في injection_container.dart
sl.registerFactoryParam(
(bookId, _) => AddEntryCubit(entryDao: sl(), bookId: bookId),
);
// الاستدعاء في الـ UI
final cubit = sl(param1: currentBookId);
هنا GetIt بيشتغل كـ "خلاط". بياخد الـ EntryDao اللي عنده أصلاً، وبياخد الـ bookId اللي إنت لسه باعته، ويطلعلك Cubit جاهز للشغل فوراً.
بعض الخدمات (زي SharedPreferences) بتحتاج وقت عشان تحمل من ذاكرة الموبايل. عشان كدة الـ initDI لازم تكون Future<void>.
// في main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await initDI(); // لازم نستنى لغاية ما كل العدة تجهز!
runApp(const MyApp());
}
لو نسينا الـ await، التطبيق ممكن يضرب (Crash) لأنه هيحاول يستخدم خدمة لسه مخلصتش تحميل.
أكبر ميزة للـ DI هي في الـ Unit Testing. إحنا نقدر "نلعب" في شنطة العدة ونشيل الشاكوش الأصلي ونحط واحد بلاستيك (Mock) عشان التست.
void main() {
test('يجب الحصول على البيانات من النسخة المزيفة', () {
final mockRepo = MockBookRepository();
// بنسجل النسخة المزيفة بدل الأصلية
sl.registerSingleton(mockRepo);
// دلوقتي أي حد يطلب IBookRepository هياخد الـ Mock!
final repo = sl();
expect(repo, isA());
});
}
عشان متتوهش، ده جدول بيوضح كل نوع تبعية بيتم التعامل معاه إزاي في المشروع:
| Layer | Service | Pattern | Lifetime |
|---|---|---|---|
| Persistence | SharedPreferences | LazySingleton | طول عمر البرنامج. |
| Database | AppDatabase | LazySingleton | طول عمر البرنامج. |
| Domain | IBookRepository | LazySingleton | طول عمر البرنامج. |
| Presentation | BooksBloc | Factory | بيموت أول ما الشاشة تتقفل. |
لما التطبيق يكبر، مينفعش نحط كل حاجة في ملف واحد. عشان كدة بنستخدم injectDashboardFeature مثلاً.
void injectDashboardFeature(GetIt sl) {
sl.registerFactory(() => DashboardCubit(
bookDao: sl(),
entryDao: sl(),
));
}
ده بيخلي كل ميزة مسؤولة عن "حقن" نفسها، وده بيريحنا جداً لما نحب نضيف ميزة جديدة أو نمسح ميزة قديمة.
حاول تلاقي الغلطة في الحالات دي:
السيناريو: بتطلب sl<MyService>() وبيطلع Error إن الخدمة مش موجودة.
الحل: اتأكد إنك سجلت الخدمة جوه initDI، وفوق السطر اللي بتطلبها فيه. الترتيب مهم!
السيناريو: سجلت MyCubit كـ Singleton.
الحل: غلط! الـ Cubit لازم يكون Factory. لو بقى Singleton، هيفضل محتفظ ببيانات قديمة من شاشات تانية حتى لو اتقفلت.
من الناحية التقنية، GetIt هو Service Locator. الفرق بسيط بس مهم:
| Concept | Service Locator (GetIt) | Pure DI (Constructor) |
|---|---|---|
| How it works | الكلاس بيطلب حاجته بنفسه من GetIt. | الكلاس بيستنى حاجته تجيله في الـ Constructor. |
| Pros | سهل جداً، مش محتاج BuildContext. | واضح جداً، التبعيات باينة من بره الشنطة. |
| Cons | بيخفي التبعيات شوية جوه الكود. | بيعمل زحمة (Boilerplate) كتير في الـ Constructors. |
في MRE CashBook، إحنا دمجنا الاتنين. بنسجل في GetIt، بس الـ Blocs بتاخد حاجتها عن طريق الـ Constructor Injection. ده بيجمع بين سهولة GetIt ووضوح الـ DI.
في اختبارات التكامل، إحنا مش بس بنجرب كلاس واحد، إحنا بنجرب "البيت كله". الـ DI بيقدر يبهدل الدنيا لو مش مظبوط، بس GetIt بيسهل لنا التبديل حتى لو الأبلكيشن شغال.
setUp(() async {
// بنصفر GetIt قبل كل تست
await sl.reset();
// بنسجل كل الخدمات المزيفة
sl.registerLazySingleton(() => MockDatabase());
sl.registerLazySingleton(() => MockPrefs());
// بنشغل باقي الـ DI الأصلي
initDI();
});
بالطريقة دي، الأبلكيشن بيفضل فاكر إنه بيكلم داتابيز حقيقية، بس في الحقيقة هو بيكلم ملف في الـ Memory بتاع التست. ده بيخليه سريع وآمن.
عشان تحافظ على شنطة عدتك نظيفة، اتبع القواعد دي:
- لا تسجل أي Widget جوه GetIt. الـ Widgets بتعتمد على الـ Context بس.
- لا تستخدم
sl.get()جوه الـ UI مباشرة؛ مرر الخدمة للـ Bloc الأول. - دائماً استخدم
LazySingletonلو الخدمة تقيلة (زي الـ DB) عشان متبدأش إلا لو حد طلبها. - دائماً افصل ملفات الـ DI لكل Feature عشان المشروع ميكبرش منك ويهرب.
- تجنب تسجيل كلاسات بتعتمد على بعض بشكل دائري (Circular Dependency).
GetIt بدل الـ Provider؟GetIt instead of Provider?GetIt ممتاز لإدارة "الخدمات" (Services)، بينما Provider أو Bloc ممتاز لإدارة "الـ UI States". الأفضل تستخدم الاتنين مع بعض زي ما عملنا في المشروع.GetIt is great for long-lived services, while Provider/Bloc is better for UI state. Using both together is the best practice.registerSingleton بدل registerLazySingleton؟registerSingleton instead of Lazy?registerSingleton لو إنت عاوز الخدمة تشتغل فوراً أول ما الأبلكيشن يفتح (زي الـ Analytics)، واستخدم Lazy لو عاوزها تشتغل بس لما حد يحتاجها لأول مرة عشان توفر رامات.registerSingleton for immediate initialization (e.g., Analytics). Use Lazy to save memory and only initialize when actually needed.عشان تتكلم بلغة المحترفين، لازم تكون عارف المصطلحات دي:
- Dependency: أي كلاس معتمد على كلاس تاني.
- Service Locator: المركز اللي بنطلب منه الخدمات (زي GetIt).
- Decoupling: إننا نفصل الكلاسات عن بعض عشان التغيير في واحد ميبوظش التاني.
- Mock: نسخة مزيفة من الكلاس بنستخدمها في الاختبارات.
- Composition Root: المكان اللي بنربط فيه كل التبعيات ببعض (زي ملف
initDI).
| Concept | Arabic Recap | English Recap |
|---|---|---|
| Initialization | بتتم مرة واحدة في الـ main.dart. | Initialized once at app startup. |
| Registration | عندنا Singletons (واحدة بس) و Factories (كتير). | Singletons vs Factories. |
| Interface | دايماً بنعتمد على الـ Interface مش الـ Implementation. | Coding to interfaces, not concretes. |
| Testing | سهل جداً نبدل الحقيقي بالمزيف. | Easy swapping for mocks. |
أحياناً بنحتاج نسجل تبعيات "مؤقتة" لشاشة معينة بس، ونمسحها أول ما نمشي. GetIt بيدينا ميزة الـ pushNewScope.
// فتح مستوى جديد
sl.pushNewScope();
sl.registerFactory(() => TemporaryCubit());
// مسح كل اللي في المستوى ده
sl.popScope();
ده بيفيد جداً في العمليات المعقدة اللي مش عاوزين دوشتها تفضل موجودة في الذاكرة طول الوقت.
[ User Request ] ──▶ [ GetIt sl ] ──▶ [ Search Toolbox ]
│
┌───────────────┴───────────────┐
▼ ▼
[ LazySingleton ] [ Factory ]
(One instance) (New instance)
Used for: DB, Repo Used for: BLoC, Cubit
│ │
└───────────────┬───────────────┘
▼
[ Injected Object ]
الرسمة دي بتلخص تدفق البيانات والطلبات جوه نظام الـ DI بتاعنا.
الـ Dependency Injection هو اللي بيخلي الكود بتاعنا "مفكوك" (Decoupled)، وده سر سهولة الصيانة والتعديل في المستقبل.