توليد الكود باستخدام injectable وbuild_runner
توليد الكود باستخدام injectable وbuild_runner
في الدرس السابق قمت بتوصيل كل الاعتماديات في GetIt يدوياً داخل دالة configureDependencies(). هذا النهج يعمل، لكنه يتدهور مع النمو: كل خدمة أو مستودع أو وحدة تحكم جديدة تضيفها تستلزم استدعاء تسجيل يدوياً. تحل مكتبة injectable هذه المشكلة بالسماح لك بتعليق الكلاسات بوسوم (annotations) ثم تفويض منطق التسجيل بالكامل إلى مولّد الكود. أما build_runner فهو أداة بناء Dart الأساسية التي تقرأ تلك الوسوم وتكتب الكود الرتيب (boilerplate) نيابةً عنك.
إضافة الاعتماديات
تحتاج ثلاث حزم. أضفها إلى pubspec.yaml:
pubspec.yaml
dependencies:
get_it: ^7.7.0
injectable: ^2.4.4 # وسوم وقت التشغيل
dev_dependencies:
injectable_generator: ^2.6.2 # مولّد الكود
build_runner: ^2.4.9 # منسّق البناء
شغّل flutter pub get بعد الحفظ. تذهب injectable إلى dependencies لأن الوسوم تعيش في كودك المُترجَم. تذهب injectable_generator وbuild_runner إلى dev_dependencies لأنهما مطلوبتان أثناء التطوير فقط، وليس في APK أو IPA الإنتاجي.
إعداد وحدة الحقن
أنشئ ملفاً — عادةً lib/core/di/injection.dart — يُعلن عن جذر حاوية الحقن. تُخبر وسمة @InjectableInit مكتبة injectable بمكان توليد كود التوصيل.
lib/core/di/injection.dart
import 'package:get_it/get_it.dart';
import 'package:injectable/injectable.dart';
import 'injection.config.dart'; // مُولَّد — غير موجود بعد
final GetIt getIt = GetIt.instance;
@InjectableInit(
initializerName: 'init', // اسم الدالة المُولَّدة
preferRelativeImports: true, // استخدام استيرادات نسبية في الملف المُولَّد
asExtension: false, // توليد دالة من المستوى الأعلى وليس extension
)
Future<void> configureDependencies() => init(getIt);
الملف injection.config.dart غير موجود بعد — build_runner يُنشئه. سيُظهر IDE خطأً أحمر حتى تُشغّل المولّد للمرة الأولى؛ هذا متوقع.
تعليق الكلاسات بوسوم الحقن
توفّر injectable أربع وسوم أساسية. طبّق واحدة منها فقط على كل كلاس تريد تسجيله:
@injectable— يُنشأ مثيل جديد في كل مرة يُحلَّ فيها النوع (تسجيل مصنع)@singleton— يُنشأ مثيل واحد فوراً عند بناء الحاوية@lazySingleton— يُنشأ مثيل واحد في أول مرة يُحلَّ فيها النوع ثم يُخزَّن@module— يُعلّم كلاساً مجرداً تُوفّر دوال getter الخاصة به تسجيلات للحزم الخارجية أو الواجهات
تعليق الخدمات والمستودعات
import 'package:injectable/injectable.dart';
// مسجَّل كمصنع — مثيل جديد في كل استدعاء
@injectable
class AuthBloc {
final AuthRepository _repo;
AuthBloc(this._repo); // injectable يقرأ معاملات المُنشئ تلقائياً
}
// مسجَّل كـ lazy singleton — يُنشأ مرة واحدة عند أول استخدام
@lazySingleton
class AuthRepository {
final ApiClient _client;
AuthRepository(this._client);
Future<User> login(String email, String password) async {
return _client.post('/auth/login', {'email': email, 'password': password});
}
}
// مسجَّل كـ eager singleton — يُنشأ فوراً عند بدء التطبيق
@singleton
class ApiClient {
final String baseUrl;
ApiClient(@Named('baseUrl') this.baseUrl);
}
@lazySingleton للكائنات الثقيلة (عملاء HTTP، اتصالات قواعد البيانات) التي يجب إنشاؤها فقط عند الحاجة. استخدم @singleton عندما يجب أن يكون الكائن جاهزاً قبل أي شيء آخر. استخدم @injectable (مصنع) لـ BLoCs وViewModels التي يجب أن تمتلك حالة جديدة لكل شاشة.تسجيل كلاسات الجهات الخارجية بـ @module
لا يمكنك تعليق كلاسات لا تملكها (مثل Dio أو SharedPreferences أو FlutterSecureStorage). استخدم كلاس @module بدلاً من ذلك:
lib/core/di/app_module.dart
import 'package:dio/dio.dart';
import 'package:injectable/injectable.dart';
import 'package:shared_preferences/shared_preferences.dart';
@module
abstract class AppModule {
// قيمة مُسمَّاة — injectable يحقنها أينما استُخدمت @Named('baseUrl')
@Named('baseUrl')
String get baseUrl => 'https://api.example.com';
// مصنع غير متزامن — build_runner يُولّد await تلقائياً
@preResolve
Future<SharedPreferences> get prefs => SharedPreferences.getInstance();
// singleton عادي من حزمة خارجية
@lazySingleton
Dio get dio => Dio(BaseOptions(baseUrl: baseUrl));
}
تُخبر وسمة @preResolve مكتبة injectable بأن هذا الـ getter يُعيد Future ويجب انتظاره قبل أن تصبح الحاوية جاهزة. لهذا السبب تُعيد configureDependencies() قيمة من نوع Future<void>.
تشغيل المولّد
بعد وضع وسوماتك في مكانها، شغّل build_runner من جذر المشروع:
الطرفية (Terminal)
# توليد لمرة واحدة (CI، ما قبل الـ commit)
dart run build_runner build --delete-conflicting-outputs
# وضع المراقبة أثناء التطوير الفعّال (يُعيد التشغيل عند كل حفظ)
dart run build_runner watch --delete-conflicting-outputs
يمسح build_runner كل ملفات Dart بحثاً عن وسوم injectable، يحلّ رسم الاعتماديات، ويكتب lib/core/di/injection.config.dart. ارفع هذا الملف المُولَّد إلى نظام التحكم بالإصدار حتى لا يضطر زملاؤك وخط CI إلى إعادة توليده من الصفر في كل عملية سحب.
--delete-conflicting-outputs للسماح لـ build_runner بتنظيف المخلّفات القديمة تلقائياً.الملف المُولَّد
نادراً ما ستحتاج إلى قراءة الملف المُولَّد، لكن فهم شكله يبني الثقة. مقتطف مبسّط يبدو هكذا:
injection.config.dart (مُولَّد تلقائياً — لا تُعدّله يدوياً)
// GENERATED CODE - DO NOT MODIFY BY HAND
Future<void> init(GetIt getIt, {String? environment}) async {
// @preResolve — يُنتظر قبل أي شيء آخر
final sharedPreferences = await SharedPreferences.getInstance();
getIt.registerSingleton<SharedPreferences>(sharedPreferences);
// @singleton
getIt.registerSingleton<ApiClient>(
ApiClient(getIt<String>(instanceName: 'baseUrl')),
);
// @lazySingleton
getIt.registerLazySingleton<AuthRepository>(
() => AuthRepository(getIt<ApiClient>()),
);
// @injectable (factory)
getIt.registerFactory<AuthBloc>(
() => AuthBloc(getIt<AuthRepository>()),
);
}
البيئات: dev وtest وprod
تدعم injectable البيئات المُسمَّاة. علّق التطبيقات البديلة بـ @Environment('test') (أو الثوابت المدمجة @dev و@prod و@test) ومرّر البيئة النشطة عند التهيئة:
تسجيلات خاصة بالبيئة
// التطبيق الإنتاجي
@prod
@LazySingleton(as: AuthRepository)
class RemoteAuthRepository implements AuthRepository { ... }
// التطبيق التجريبي / المحاكي
@test
@LazySingleton(as: AuthRepository)
class FakeAuthRepository implements AuthRepository { ... }
// main.dart
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await configureDependencies(environment: Environment.prod);
runApp(const MyApp());
}
الخلاصة
تعمل مكتبتا injectable وbuild_runner كفريق واحد: أنت تضع الوسوم والمولّد يوصّل. سير العمل هو: إضافة وسوم ← تشغيل dart run build_runner build ← رفع الملف المُولَّد ← استخدام getIt<T>() كالمعتاد. النتيجة هي حاوية حقن آمنة الأنواع بالكامل، تُكتشف أخطاؤها وقت الترجمة، مع صفر من الكود الرتيب المكتوب يدوياً. ادمجها مع دعم البيئات و@module للأنواع الخارجية، وسيظل رسم اعتمادياتك قابلاً للصيانة بأي حجم.
@injectable = مصنع، @singleton = singleton فوري، @lazySingleton = singleton كسول، @module = تسجيل أنواع لا تملكها. شغّل build_runner build بعد كل تغيير في الوسوم. ارفع ملف injection.config.dart المُولَّد.