بسم الله و الحمد لله و الصلاة و السلام على رسول الله الحمد لله الذي علم بالقلم علم الإنسان ما لم يعلم و الصلاة والسلام على خير معلم الناس الخير محمد و بعد : 
 

إن شاء الله هنبدأ سلسلة Flutter inDepth و السلسلة هدفها التعمق شوية في بعض مفاهيم flutter و دراستها بتعمق بعيدا عن السطحية فا نبدأ السلسلة بحاجة بسيطة

بسم الله :  


الأهداف النهائية 

بنهاية الخطة دي هتكون قادر على: 

  • فهم مفهوم الـ Dependency Injection بشكل عام 
  • التفرقة بين الأنواع المختلفة (Constructor Injection, Service Locator, etc.) 
  • استخدام أشهر مكتبات DI في Flutter مثل: 
    • get_it 
    • get_it + injectable 
  • بناء Architecture نظيف زي Clean Architecture أو MVVM/MVC 
  • كتابة كود Testable باستخدام DI 
  • فهم العلاقة بين DI و State Management 

نبدأ على بركة الله 
طيب علشان افهم يعني إيه Dependency Injection لازم افهم الأول ال Dependency. 


ما هي ال Dependency؟

Dependency معناها "اعتماد".
 في البرمجة، أي class بيحتاج كائن من class تاني علشان يشتغل بطريقة صحيحة  بنقول إنه يعتمد عليه.
مثال  

class ApiService {
  Future<String> fetchData() async => "Data from API";
}
class UserRepository {
  final ApiService apiService;
  UserRepository(this.apiService); // ← dependency injection
  Future<void> getUserData() async {
    String data = await apiService.fetchData();
    print("User data: $data");
  }
}

وبكدا UserRepository ما يعرفش حاجة عن تفاصيل ApiService، بس بيعتمد عليه عشان يشتغل. 

لو حبيت تغير ApiService بكلاس تاني مثلاً MockApiService، تقدر تعمل ده من غير ما تغير UserRepository


لماذا ال Dependency مهمة؟ 

  • بتحقق مبدأ المسؤولية الواحدة (Single Responsibility Principle) لأن كل Class بيركز على شغله بس. 
  • بتساعد على الاختبار لأنك تقدر ت inject fake أو mock dependencies بسهولة. 
  • بتخلي الكود قابل لإعادة الاستخدام وسهل تغير فيه براحتك. 

ما هو الـ Injection؟  

ال Injection هو: "تمرير" أو "حقن" الـ Dependency إلى ال Class بدل ما هو ينشئها بنفسه. 

لو الClass نفسه بينشئ الاعتماد: 

class UserRepository {
  final ApiService apiService = ApiService(); // tightly coupled ❌
}

مشكلة ده: 

  • مقدرتش أبدل ApiService بـ MockApiService للاختبار. 
  • كل ما الكلاس يعتمد على dependency جديدة، بيزيد الـ coupling. 

لكن باستخدام Dependency Injection

class UserRepository {
  final ApiService apiService;
  UserRepository(this.apiService); // dependency injected from outside
}

الفرق بين Tightly Coupled و Loosely Coupled 

Tightly Coupled Loosely Coupled
التعريف كلاس يعرف كل التفاصيل عن Dependency كلاس لا يعرف غير (interface)
قابل للاختبار ❌ صعب ✅ سهل
قابل لإعادة الاستخدام ❌ محدود ✅ عالي

أنواع Dependency Injection

هنا هنستعرض أنواع ال Dependencies عن طريق عرض الكود و شرح مميزاته وعيوبه

1. Constructor Injection  

class AuthService {
  void login() => print("Login");
}
class LoginScreen {
  final AuthService authService;
  LoginScreen(this.authService); // constructor injection
}

المميزات: 

  • واضح وسهل 
  • يسهل قراءة الاعتماد من constructor 
  • immutable (final) 

 العيوب: 

  • لو الclass بيعتمد على 6 خدمات، constructor هيبقى كبير

2. Setter Injection 

class LoginScreen {
  late AuthService authService;
  void setAuthService(AuthService auth) {
    this.authService = auth;
  }
}

المميزات: 

  • مرن 
  • تقدر تغير الـ dependency في أي وقت 

 العيوب: 

  • ممكن تنسى ت inject قبل ما تستخدمه 
  • أقل أمانًا من constructor injection 

سؤال لماذا يٌعتبر أقل أمان و ما هو المقصود ب الأمان هنا ؟ 


أولًا: المقصود بـ "الأمان" هنا إيه؟ 

في عالم البرمجة، لما نقول أقل أمانًا بنقصد:
 هل الكود يمنع المبرمج (أو نفسه كمستخدم داخلي) من الوقوع في الأخطاء أو النسيان؟ 

بمعنى: 

هل في طريقة تمنعك من نسيان حقن dependency قبل استخدامها؟
 وهل نقدر نضمن إن الـ dependency موجودة ومتاحة وقت تشغيل الكلاس؟ 


3. Constructor Injection (الأكثر أمانًا) 

 الفكرة: 

  • الاعتماد يتم حقنه إجباريًا وقت إنشاء الكائن. 
  • لا يمكن استخدام الكلاس من غير ما تحقنه. 
class AuthService {
  void login() => print("login");
}
class LoginScreen {
  final AuthService authService;
  LoginScreen(this.authService); // ← لازم تمرره وقت الإنشاء
  void loginUser() {
    authService.login(); // مضمون إنها موجودة
  }
}

الفائدة هنا

  • الـ authService final → يعني لا يمكن تغييره بعد إنشائه. 
  • ما تقدرش تعمل LoginScreen() من غير ما تمرر AuthService → compiler يجبرك. 
  • أقل عرضة لنسيان أو استخدام dependency قبل ما تكون جاهزة. 

4. Setter Injection (أقل أمانًا) 

 الفكرة: 

  • الاعتماد يتم حقنه لاحقًا بعد إنشاء الكائن، غالبًا من خلال دالة set أو property. 

 مثال: 

class LoginScreen {
  late AuthService authService; // not final!
  void loginUser() {
    authService.login(); // خطر! ممكن تكون null أو لم تُحقن بعد
  }
}

العيوب هنا

  • مفيش ضمان إن authService تم حقنه قبل استدعاء loginUser() 
  • ممكن تنسى تمرره وتبدأ تستخدم الكلاس → يحصل خطأ وقت التشغيل (runtime error) 
  • لو حد نادى loginUser() قبل حقن الخدمة، يحصل null exception 

 خلاصة :  

"Constructor Injection guarantees that a class will never exist in an invalid state, while Setter Injection relies on the developer's discipline." 

5. Service Locator (زي get_it) 

final sl = GetIt.instance;
void setup() {
  sl.registerSingleton<AuthService>(AuthService());
}
class LoginScreen {
  final authService = sl<AuthService>();
}

المميزات: 

  • مرن جدًا وسهل الاستخدام 
  • مناسب للمشاريع الكبيرة 

خلينا نتعمق أكثر في ال service locator:  

  • تفهم يعني إيه Service Locator وإزاي بيختلف عن DI التقليدي. 
  • تميز بين أنواع التسجيل: registerSingleton, registerLazySingleton, registerFactory. 
  • تعرف إزاي تستخدم get_it مع testing و clean architecture. 

أولًا: ما هو ال  Service Locator؟ 

Service Locator هو pattern بيخليك تسجل الكائنات (services, repositories, etc.) في كائن مركزي، وتسترجعها في أي مكان من المشروع من غير ما تمررها يدويًا. 

 بمعنى :
تخيل ان عندك مخزن كبير بتخزن فيه كل ال dependencies  اللي كل كلاس محتاجها و  

بدال ما تمرر dependencies لكل class، كل class يطلب الخدمة اللي محتاجها من (المخزن  = GetIt)


ثانيًا, الفرق بين registerSingleton, registerLazySingleton, registerFactory

  • registerSingleton 
  • registerLazySingleton 
  • registerFactory 

registerSingleton<T>(T instance) 

الفكرة: 

أنت بنفسك بتنشئ الكائن مرة واحدة فقط، وتسجله في get_it.
 كل مرة تستدعي getIt<T>() هيرجعلك نفس الكائن القديم

 أهم الخصائص: 

خاصية القيمة
وقت الإنشاء مباشرة وقت التسجيل
عدد الـ instances 1 فقط
يعيد نفس الكائن دائمًا؟ ✅ نعم
suitable for Config, SharedPreferences, NavigationService

 مثال عملي: 

final apiClient = ApiClient(baseUrl: 'https://example.com');
getIt.registerSingleton<ApiClient>(apiClient);

أو 

getIt.registerSingleton<ApiClient>(ApiClient(baseUrl: 'https://example.com'));
 

registerLazySingleton(() => T()) 

الفكرة: 

لا يبني الكائن إلا أول مرة لما حد يطلبه. بعد كده، بيحتفظ بنفس الكائن طول مدة تشغيل التطبيق. 

 أهم الخصائص: 

خاصية القيمة
وقت الإنشاء عند أول طلب (Lazy)
عدد الـ instances 1 فقط
يعيد نفس الكائن دائمًا؟ ✅ نعم
suitable for Repositories, UseCases, Services

 مثال عملي: 

getIt.registerLazySingleton<AuthRepository>(() => AuthRepositoryImpl(getIt<ApiClient>()));

registerFactory(() => T())

الفكرة: 

كل مرة تستدعي getIt<T>()، بيرجعلك كائن جديد تمامًا

 أهم الخصائص: 

خاصية القيمة
وقت الإنشاء عند كل طلب
عدد الـ instances جديد في كل مرة
يعيد نفس الكائن دائمًا؟ ❌ لا
suitable for Cubits, BLoCs, FormControllers

 مثال عملي: 

getIt.registerFactory<LoginCubit>(() => LoginCubit(getIt<AuthRepository>()));

مقارنة شاملة

النوع وقت الإنشاء عدد الـ instances يعيد نفس الـ object؟ مناسب لـ
registerSingleton عند التسجيل 1 ✅ نعم إعدادات التطبيق، SharedPreferences
registerLazySingleton عند أول طلب 1 ✅ نعم Repositories, Services
registerFactory عند كل طلب ❌ لا Cubit, Bloc, Form Controllers

 ملحوظات مهمة

 متى تستخدم كل واحد منهم؟ 

الحالة استخدم
عندك كائن Shared وعايزه يكون موجود طول الوقت registerSingleton
عايز توفر في الموارد، وماتنشئش الكائن إلا عند الحاجة registerLazySingleton
محتاج كائن جديد كل مرة (زي Cubit لكل شاشة) registerFactory

متى استخدم registerSingleton و registerLazySingleton و registerFactory يكون Over Engineering؟
 بمعنى: "أكثر من اللازم" وغير مبرر للمشروع أو المرحلة اللي فيه.  

أولاً: ما هو الـ Over Engineering؟ 

هو إنك تستخدم حلول معقدة أو كبيرة أكتر من الحاجة الحقيقية للمشروع، وده بيأدي إلى: 

  • تعقيد غير ضروري. 
  • صعوبة في القراءة والصيانة. 
  • بطء في التطوير. 

 القاعدة العامة: 

 "Don't abstract until you need to."
 يعني: ما تبدأش تستخدم DI و Singleton و Factory بشكل كامل إلا لو في فعلاً حاجات محتاجة كده. 

حالات يكون فيها استخدامهم Over Engineering: 

1. لو المشروع صغير أو MVP (نسخة أولية) 

مثلاً: تطبيق فيه 3 شاشات بسيطة، وRepository واحد فقط. 

 الأفضل: 

final repo = MyRepository(); 
final cubit = MyCubit(repo); 

 لو استخدمت: 

getIt.registerLazySingleton<MyRepository>(() => MyRepositoryImpl()); 
getIt.registerFactory<MyCubit>(() => MyCubit(getIt<MyRepository>())); 

  ده Over Engineering، لأنك كده زوّدت abstraction ومفيش أي تعقيد محتاجه. 

2. لو مش عندك إعادة استخدام للكائن في أكثر من مكان 

لو مثلًا عندك UserCubit بيستخدم في شاشة واحدة بس، وبيتاخد داخل الـ BlocProvider مباشرة. 

عادي تبنيه بداخل الشاشة: 

BlocProvider( 
  create: (_) => UserCubit(repo), 
) 
 لو عملت: 
getIt.registerFactory(() => UserCubit(getIt())); 

→ كده Over Engineering طالما مش هتعيد استخدامه ولا محتاج inject. 

3. لو أنت بتسجل Singleton لحاجة مافيهاش State مشترك 

زي مثلًا: 

getIt.registerSingleton<TempModel>(TempModel()); 

بس TempModel مش بيستخدم في أكتر من مكان، ومش بتمثل حالة مشتركة، فده مش منطقي. 

4. لو استخدمت Factory لحاجة ثابتة المفروض Singleton 

getIt.registerFactory(() => AppConfig()); 

→ المفروض تبقى Singleton مش Factory، لأن AppConfig المفروض واحد في كل مكان.
 يعني Factory هنا هتخلي عندك نسخ مختلفة بدون داعي. 

5. لو عندك كائن مافيش فيه Logic أو Dependencies أصلاً 

تسجل كائن صغير لا يحتوي علي شيء مثل: 

class BasicModel {} 
getIt.registerFactory(() => BasicModel()); 

ده Overkill.  

 متى يكون الاستخدام "ليس Over Engineering" بل هو ضرورة؟ 

الحالة النوع المناسب
Shared State – موجود في كذا شاشة registerSingleton
Service أو Repository – هيستخدم في أكتر من كلاس registerLazySingleton
Cubit أو Bloc – له دورة حياة مرتبطة بالشاشة registerFactory

  خلاصة مبسطة للحالات التي يمكن استخدام DI بها

الحالة أفضل حل ليه؟
مشروع صغير – شاشة أو 2 بدون DI البساطة أولًا
كائن بيستخدم في شاشة واحدة ابنيه يدويًا مفيش إعادة استخدام
كائن عالمي أو إعدادات Singleton لازم يكون نسخة واحدة
كائن يُنشأ حسب الحاجة LazySingleton توفير موارد
Cubit لشاشة Factory علشان كل مرة كائن جديد

نصيحة عملية

ابدأ بسيط، ولما المشروع يكبر أو يزيد التعقيد: 

 "Refactor to DI when it hurts." 

طيب هنا هتظهر مشكلة جديدة لو انا شغال Clean Architecture يعني  

64e02f452e6fda0ab99783dbbdf90dc5.png

كده انا هسجل كل شوية على الاقل 3 او 4 في كل Feature ب ايدي كل شوية 
مثال :


// //----------------------------for home----------------------------
getIt.registerLazySingleton<HomeRemoteDataSource>(
  () => HomeRemoteDataSource(getIt<DioConsumer>()),
);
getIt.registerLazySingleton<HomeRepoImpl>(
  () => HomeRepoImpl(getIt<HomeRemoteDataSource>()),
);
getIt.registerLazySingleton<HomeUseCase>(
  () => HomeUseCase(getIt<HomeRepoImpl>()),
);
getIt.registerFactory(() => HomeCubit(getIt<HomeUseCase>())); 

طيب و الحل ؟ 
دمج get_it مع injectable للتوليد التلقائي للـ dependencies
 ودي المرحلة اللي بنوصل فيها لدرجة عالية من التنظيم، بنخلي التوليد تلقائي وبالتالي بنقلل الكود الـ boilerplate يدوي، وبنخلي الكود scalable وسهل التعديل عليه.  

كلمة boilerplate في البرمجة معناها: 

"الكود المتكرر اللي لازم تكتبه كل مرة علشان تعمل حاجة معينة، رغم إنه مش بيضيف منطق جديد للبرنامج." 

الحاجات اللي هنستخدمها: 

  • get_it: كـ Service Locator. 
  • injectable: لتوليد كود تسجيل الـ dependencies أوتوماتيكيا. 
  • build_runner: عشان يعمل generate للكود وقت الـ build. 

ملف ال injection.dart هيكون عبارة عن: 

import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'injectable.config.dart';
final GetIt getIt = GetIt.instance;
@InjectableInit()
Future<void> configureDependencies() async {
  await getIt.init(); // ينفذ الكود المتولد تلقائيًا من injectable
}

ما هو injectable؟ 

injectable هي مكتبة Dart بتستخدم Code Generation (عبر build_runner) علشان تولد الكود الخاص بتسجيل dependencies في get_it تلقائيًا، بناءً على أنوتيشن @injectable, @singleton, @lazySingleton وغيرها. 

بدال ما تكتب getIt.registerSingleton(...) بنفسك، بتحط annotation على الكلاس، وهو يولد التسجيلات تلقائيًا. 

 فوائد injectable 

الفائدة الشرح
تقليل الـ boilerplate متكتبش التسجيلات يدويًا
أمان في التوليد بيراجع الـ dependency tree، وأي خطأ يظهرلك في build
تنظيم المشاريع الكبيرة بيخلي ملف التسجيل منعزل ومنظم
متكامل مع get_it و mockito وبيشتغل كويس مع أي Architecture زي Clean Architecture

 كيف يعمل injectable بالتفصيل؟

1. تحط Annotation على الـ class: 

@injectable 
class AuthService { 
  void login() => print("login..."); 
} 

2. في كلاس (injection.dart): 

import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'injectable.config.dart';
final GetIt getIt = GetIt.instance;
@InjectableInit()
Future<void> configureDependencies() async {
  await getIt.init(); // ينفذ الكود المتولد تلقائيًا من injectable
}
 

3. تشغل build_runner: 

flutter pub run build_runner build --delete-conflicting-outputs 

 كده، injectable هيولد ملف اسمه injection.config.dart، فيه كل التسجيلات تلقائيًا. 

 أنواع الـ annotations في injectable (شرح متعمق) 

🔸 @injectable 

تستخدم لما تحب injectable يسجل الclass تلقائيًا باستخدام النوع الافتراضي (غالبًا factory) 


@injectable 
class LoginCubit { 
  final AuthService auth; 
 
  LoginCubit(this.auth); 
} 

النتيجة: يتم توليد كود registerFactory<LoginCubit>(() => LoginCubit(auth)); 

 

🔸 @singleton 

تسجّل كـ Singleton → يتم إنشاء الكائن مرة واحدة فقط، فورًا. 

@singleton 
class LoggerService {} 

 يتم إنشاء الكائن مباشرة أثناء configureDependencies(). 

 

🔸 @lazySingleton 

نفس فكرة singleton، لكن يتم إنشاؤه عند أول استخدام فقط

@lazySingleton 
class ApiService {}

 

✅ مفيد لو عندك خدمة ثقيلة ومش ضروري تبدأ بيها. 

🔸 @factoryMethod 

تستخدم لو عندك كائن مش بتتحكم في الكلاس بتاعه (مثلاً مكتبة خارجيّة زي Dio). 

بيتحط داخل @module class. 


@module 
abstract class RegisterModule { 
  @lazySingleton 
  Dio dio() => Dio(BaseOptions(baseUrl: "https://api.example.com")); 
} 

🔸 @Named / @Named("custom") 

تستخدم لو عندك أكثر من implementation لنفس الـ interface. 


abstract class AuthStrategy { 
  void login(); 
} 
 
@Named("Google") 
@injectable 
class GoogleAuth implements AuthStrategy { 
  @override 
  void login() => print("Google login"); 
} 
 

🔸 @Named("Facebook") 

@injectable 
class FacebookAuth implements AuthStrategy { 
  @override 
  void login() => print("Facebook login"); 
} 

وبعدين في الكلاس اللي بيحتاج AuthStrategy: 


@injectable 
class AuthController { 
  AuthController(@Named("Google") AuthStrategy auth); 
} 

🔸 @preResolve 

تستخدم لما يكون في dependency لازم تنتظر تنفيذه async أثناء configureDependencies()  

@module 
abstract class RegisterModule { 
  @preResolve 
  Future<SharedPreferences> prefs() async => 
      await SharedPreferences.getInstance(); 
} 

 أسئلة شائعة

  • هل لازم كل ال classes أكتب عليها @injectable؟ 

 نعم، لو عايز injectable تسجلها تلقائيًا، لازم تكتب عليها annotation. 

  • هل أقدر أحقن dependencies جوه Widget؟ 

 نعم، تقدر تستخدم getIt<MyClass>() داخل أي widget، لكن الأفضل تحقنها في ViewModel أو Cubit وتمررها للواجهة. 

  • لو حصل conflict أو لم يتم توليد الكود؟ 

 شغّل الأمر ده دايمًا: 

flutter pub run build_runner build --delete-conflicting-outputs 

✅ تلخيص جدول لأنواع الـ @ في injectable 

Annotation السلوك يستخدم في
@injectable default registration أي كلاس قابل للحقن
@singleton create once (eager) الخدمات الأساسية
@lazySingleton create once (lazy) الخدمات الثقيلة
@factoryMethod توليد باستخدام method الكائنات من مكتبات خارجية
@module تعريف مجموعة factory methods configuration classes
@Named("...") تمييز أنواع متعددة لما عندك أكثر من implementation
@preResolve async singleton before init مثل SharedPreferences

 نلقاكم في بقية السلسلة بإذن الله 👋