بسم الله و الحمد لله و الصلاة و السلام على رسول الله الحمد لله الذي علم بالقلم علم الإنسان ما لم يعلم و الصلاة والسلام على خير معلم الناس الخير محمد و بعد :
إن شاء الله هنبدأ سلسلة 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 يعني
كده انا هسجل كل شوية على الاقل 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 |
نلقاكم في بقية السلسلة بإذن الله 👋
Discussion