أوضحت صفحة أساسيات Dagger كيف يمكن أن يساعدك Dagger في برمجة تضمين التبعية في تطبيقك. وباستخدام Dagger، لا تحتاج إلى كتابة رمز نموذجي مملة وعرضة للخطأ.
ملخّص أفضل الممارسات
- استخدِم حقن دالة الإنشاء مع
@Inject
لإضافة أنواع إلى الرسم البياني Dagger كلما أمكن ذلك. في حال عدم إجراء ذلك:- استخدِم
@Binds
لإبلاغ Dagger بالتنفيذ الذي يجب أن تتوفر في الواجهة. - استخدِم
@Provides
لإبلاغ Dagger بكيفية تقديم صفوف لا يملكونها في مشروعك.
- استخدِم
- يجب الإفصاح عن الوحدات مرة واحدة فقط في المكوِّن.
- قم بتسمية التعليقات التوضيحية للنطاق بناءً على
مدة استخدام التعليق التوضيحي. تشمل الأمثلة
@ApplicationScope
و@LoggedUserScope
و@ActivityScope
.
إضافة التبعيات
لاستخدام Dagger في مشروعك، أضِف هذه التبعيات إلى تطبيقك في
ملف build.gradle
. يمكنك العثور على أحدث إصدار من Dagger في مشروع GitHub.
Kotlin
plugins { id 'kotlin-kapt' } dependencies { implementation 'com.google.dagger:dagger:2.x' kapt 'com.google.dagger:dagger-compiler:2.x' }
Java
dependencies { implementation 'com.google.dagger:dagger:2.x' annotationProcessor 'com.google.dagger:dagger-compiler:2.x' }
لعبة Dagger في Android
فكّر في مثال لتطبيق Android يحتوي على الرسم البياني للتبعية من الشكل 1.
في نظام Android، يمكنك عادةً إنشاء رسم بياني من Dagger موجود في فئة التطبيق
لأنك تريد ظهور مثيل من الرسم البياني في الذاكرة طالما كان التطبيق قيد التشغيل. وبهذه الطريقة، يتم إرفاق الرسم البياني بمراحل نشاط التطبيق. في بعض الحالات، قد تحتاج أيضًا إلى توفير سياق التطبيق في الرسم البياني. لتنفيذ هذا الإجراء، ستحتاج أيضًا إلى أن يكون الرسم البياني في الصف Application
. تتمثل إحدى ميزات هذا النهج
في أنّ الرسم البياني متاح لفئات أُطر عمل Android الأخرى.
بالإضافة إلى ذلك، تبسّط هذه الميزة عملية الاختبار من خلال السماح لك باستخدام فئة Application
مخصّصة في الاختبارات.
بما أنّ الواجهة التي تنشئ الرسم البياني تتضمن تعليقات توضيحية باستخدام @Component
، يمكنك تسميتها ApplicationComponent
أو ApplicationGraph
. عادةً ما تحتفظ بمثال لهذا المكوّن في فئة Application
المخصّصة وتستدعيه في كل مرة تحتاج فيها إلى الرسم البياني للتطبيق، كما هو موضّح في مقتطف الرمز التالي:
Kotlin
// Definition of the Application graph @Component interface ApplicationComponent { ... } // appComponent lives in the Application class to share its lifecycle class MyApplication: Application() { // Reference to the application graph that is used across the whole app val appComponent = DaggerApplicationComponent.create() }
Java
// Definition of the Application graph @Component public interface ApplicationComponent { } // appComponent lives in the Application class to share its lifecycle public class MyApplication extends Application { // Reference to the application graph that is used across the whole app ApplicationComponent appComponent = DaggerApplicationComponent.create(); }
ونظرًا لأن النظام يستند إلى بعض فئات إطار عمل Android، مثل الأنشطة والأجزاء،
لا يمكن لتطبيق Dagger إنشاء هذه الفئات نيابةً عنك. بالنسبة إلى الأنشطة على وجه التحديد، يجب إدخال أي رمز إعداد إلى طريقة onCreate()
.
هذا يعني أنّه لا يمكنك استخدام التعليق التوضيحي @Inject
في الدالة الإنشائية للفئة (إدخال دالة الإنشاء) كما فعلت في الأمثلة السابقة. عليك بدلاً من ذلك استخدام ميزة "حقن الحقول".
بدلاً من إنشاء التبعيات التي يتطلبها النشاط في الطريقة onCreate()
،
تريد أن يملأ Dagger هذه التبعيات نيابة عنك. ولإدخال الحقول، يتم بدلاً من ذلك تطبيق التعليق التوضيحي @Inject
على الحقول التي تريد الحصول عليها من الرسم البياني لـ Dagger.
Kotlin
class LoginActivity: Activity() { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject lateinit var loginViewModel: LoginViewModel }
Java
public class LoginActivity extends Activity { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject LoginViewModel loginViewModel; }
ولتبسيط الأمر، فإن السمة LoginViewModel
ليست نموذج عرض
لمكونات Android الهندسية، بل هي مجرّد فئة عادية تعمل كنموذج عرض.
لمزيد من المعلومات حول كيفية إدخال هذه الفئات، يمكنك الاطّلاع على الرمز
في تنفيذ Android Blueprints Dagger الرسمي، في
فرع dev-dagger.
أحد الاعتبارات الخاصة بـ Dagger هو أن الحقول التي تم حقنها لا يمكن أن تكون خاصة. يجب أن يكون لديها مستوى رؤية خاص بالحزمة على الأقل كما في الرمز السابق.
أنشطة الحقن
يحتاج Dagger إلى معرفة أنّ LoginActivity
عليه الوصول إلى الرسم البياني لتقديم ViewModel
التي يطلبها. في صفحة أساسيات Dagger، استخدمت واجهة @Component
للحصول على عناصر من الرسم البياني من خلال عرض الدوال بنوع النتيجة الذي تريد الحصول عليه من الرسم البياني. في هذه الحالة، يجب إخبار Dagger عن أي كائن (LoginActivity
في هذه الحالة) يتطلب إدخال تبعية. لذلك، تعرض دالة تأخذ كمعلمة الكائن الذي يطلب الحقن.
Kotlin
@Component interface ApplicationComponent { // This tells Dagger that LoginActivity requests injection so the graph needs to // satisfy all the dependencies of the fields that LoginActivity is requesting. fun inject(activity: LoginActivity) }
Java
@Component public interface ApplicationComponent { // This tells Dagger that LoginActivity requests injection so the graph needs to // satisfy all the dependencies of the fields that LoginActivity is injecting. void inject(LoginActivity loginActivity); }
تخبر هذه الدالة Dagger بأنّ LoginActivity
يريد الوصول إلى الرسم البياني وتطلب منه حقن البيانات. يحتاج Dagger إلى استيفاء جميع التبعيات التي يتطلبها
LoginActivity
(LoginViewModel
بتبعياتها الخاصة).
إذا كان لديك العديد من الفئات التي تطلب إدخال البيانات، يجب الإفصاح عنها جميعًا في المكوِّن على وجه الخصوص بنوعها بالضبط. على سبيل المثال، إذا طلبتَ حقنًا من خلال
LoginActivity
وRegistrationActivity
، سيكون لديك طريقتَان
inject()
بدلاً من طريقة عامة تغطي كلتا الحالتين. لا تُعلم طريقة inject()
العامة لشركة Dagger بما يجب تقديمه. يمكن أن يكون للدوال
في الواجهة أي اسم، ولكن استدعائها inject()
عندما
تتلقّى الكائن لإدخالها كمَعلمة، وهو اصطلاح في Dagger.
لحقن كائن في النشاط، يجب استخدام appComponent
المحددة في فئة Application
واستدعاء الطريقة inject()
، وتمرير حالة من النشاط الذي يطلب الحقن.
عند استخدام الأنشطة، أدخِل Dagger في
طريقة onCreate()
الخاصة بالنشاط قبل استدعاء super.onCreate()
لتجنّب المشاكل
المتعلقة باستعادة الأجزاء. خلال مرحلة الاستعادة في super.onCreate()
،
يؤدي أحد الأنشطة إلى إرفاق أجزاء قد تحتاج إلى الوصول إلى روابط الأنشطة.
عند استخدام الأجزاء، أدخِل Dagger في الطريقة onAttach()
للجزء. في هذه الحالة، يمكن إجراء ذلك قبل أو بعد الاتصال برقم super.onAttach()
.
Kotlin
class LoginActivity: Activity() { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { // Make Dagger instantiate @Inject fields in LoginActivity (applicationContext as MyApplication).appComponent.inject(this) // Now loginViewModel is available super.onCreate(savedInstanceState) } } // @Inject tells Dagger how to create instances of LoginViewModel class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... }
Java
public class LoginActivity extends Activity { // You want Dagger to provide an instance of LoginViewModel from the graph @Inject LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { // Make Dagger instantiate @Inject fields in LoginActivity ((MyApplication) getApplicationContext()).appComponent.inject(this); // Now loginViewModel is available super.onCreate(savedInstanceState); } } public class LoginViewModel { private final UserRepository userRepository; // @Inject tells Dagger how to create instances of LoginViewModel @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } }
لنخبر Dagger بكيفية تقديم بقية التبعيات لإنشاء الرسم البياني:
Kotlin
class UserRepository @Inject constructor( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... } class UserLocalDataSource @Inject constructor() { ... } class UserRemoteDataSource @Inject constructor( private val loginService: LoginRetrofitService ) { ... }
Java
public class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; @Inject public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } } public class UserLocalDataSource { @Inject public UserLocalDataSource() {} } public class UserRemoteDataSource { private final LoginRetrofitService loginRetrofitService; @Inject public UserRemoteDataSource(LoginRetrofitService loginRetrofitService) { this.loginRetrofitService = loginRetrofitService; } }
وحدات الخنجر
في هذا المثال، أنت تستخدم مكتبة الشبكات التحديث.
يعتمد UserRemoteDataSource
على LoginRetrofitService
. ومع ذلك،
تختلف طريقة إنشاء مثيل LoginRetrofitService
عما كنت تفعله حتى الآن. وهو ليس مثيلاً للفئة، بل هو نتيجة استدعاء Retrofit.Builder()
وتمرير معلَمات مختلفة لضبط خدمة تسجيل الدخول.
بصرف النظر عن تعليق @Inject
التوضيحي، هناك طريقة أخرى لإخبار Dagger بكيفية تقديم
مثال لفئة: المعلومات الموجودة داخل وحدات Dagger. وحدة Dagger هي فئة
لها تعليقات توضيحية باستخدام @Module
. يمكنك هناك تحديد التبعيات باستخدام
تعليق @Provides
التوضيحي.
Kotlin
// @Module informs Dagger that this class is a Dagger Module @Module class NetworkModule { // @Provides tell Dagger how to create instances of the type that this function // returns (i.e. LoginRetrofitService). // Function parameters are the dependencies of this type. @Provides fun provideLoginRetrofitService(): LoginRetrofitService { // Whenever Dagger needs to provide an instance of type LoginRetrofitService, // this code (the one inside the @Provides method) is run. return Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService::class.java) } }
Java
// @Module informs Dagger that this class is a Dagger Module @Module public class NetworkModule { // @Provides tell Dagger how to create instances of the type that this function // returns (i.e. LoginRetrofitService). // Function parameters are the dependencies of this type. @Provides public LoginRetrofitService provideLoginRetrofitService() { // Whenever Dagger needs to provide an instance of type LoginRetrofitService, // this code (the one inside the @Provides method) is run. return new Retrofit.Builder() .baseUrl("https://example.com") .build() .create(LoginService.class); } }
الوحدات هي طريقة لتغليف المعلومات دلاليًا حول كيفية تقديم الكائنات. كما ترى، لقد طلبت من الفئة NetworkModule
تجميع منطق توفير الكائنات ذات الصلة بالشبكات. في حال توسيع التطبيق، يمكنك أيضًا إضافة كيفية توفير OkHttpClient
هنا أو كيفية ضبط Gson أو Moshi.
تمثّل تبعيات الطريقة @Provides
معلَمات تلك الطريقة. بالنسبة إلى
الطريقة السابقة، يمكن توفير LoginRetrofitService
بدون تبعيات
لأن الطريقة لا تحتوي على معلمات. إذا كنت قد عرّفت OkHttpClient
على أنّه مَعلمة، سيحتاج Dagger إلى تقديم مثيل OkHttpClient
من الرسم البياني لتلبية تبعيات LoginRetrofitService
. مثلاً:
Kotlin
@Module class NetworkModule { // Hypothetical dependency on LoginRetrofitService @Provides fun provideLoginRetrofitService( okHttpClient: OkHttpClient ): LoginRetrofitService { ... } }
Java
@Module public class NetworkModule { @Provides public LoginRetrofitService provideLoginRetrofitService(OkHttpClient okHttpClient) { ... } }
لكي يتعرّف الرسم البياني Dagger على هذه الوحدة، عليك إضافتها إلى واجهة @Component
على النحو التالي:
Kotlin
// The "modules" attribute in the @Component annotation tells Dagger what Modules // to include when building the graph @Component(modules = [NetworkModule::class]) interface ApplicationComponent { ... }
Java
// The "modules" attribute in the @Component annotation tells Dagger what Modules // to include when building the graph @Component(modules = NetworkModule.class) public interface ApplicationComponent { ... }
تتمثل الطريقة الموصى بها لإضافة أنواع إلى الرسم البياني لـ Dagger في استخدام حقن الدالة الإنشائية (أي من خلال التعليق التوضيحي @Inject
على الدالة الإنشائية للفئة).
في بعض الأحيان، لا يمكن ذلك ويتعين عليك استخدام وحدات Dagger. أحد الأمثلة هو عندما تريد أن يستخدم Dagger نتيجة العملية الحسابية لتحديد كيفية
إنشاء مثيل لكائن. وعندما تحتاج إلى توفير مثيل من هذا النوع، تشغّل Dagger الرمز البرمجي داخل طريقة @Provides
.
هذا هو الشكل الذي يبدو عليه الرسم البياني Dagger في المثال الآن:
نقطة الدخول إلى الرسم البياني هي LoginActivity
. بما أنّ LoginActivity
يُدخل
LoginViewModel
، ينشئ Dagger رسمًا بيانيًا يعرف كيفية تقديم مثيل LoginViewModel
، وبشكل متكرر، لتبعياته. يعرف Dagger كيفية القيام بذلك بسبب
التعليق التوضيحي @Inject
على الدالة الإنشائية للفئات.
داخل ApplicationComponent
الذي أنشأه Dagger، هناك طريقة من نوع المصنع
للحصول على مثيلات من جميع الفئات التي يعرف كيفية تقديمها. في هذا المثال، تم إدراج تفويض Dagger إلى NetworkModule
في ApplicationComponent
للحصول على مثال عن LoginRetrofitService
.
نطاقات خناجر
تم ذكر النطاقات في صفحة أساسيات Dagger كطريقة للحصول على مثال فريد لنوع ما في المكوِّن. وهذا ما يعنيه تحديد نطاق نوع لدورة حياة المكون.
بما أنّك قد تحتاج إلى استخدام UserRepository
في ميزات أخرى في التطبيق وربما لا تريد إنشاء عنصر جديد في كل مرة تحتاج إليه، يمكنك تخصيص هذا العنصر ليكون مثيلاً فريدًا للتطبيق بأكمله. وهو الأمر نفسه بالنسبة إلى LoginRetrofitService
: قد يكون إنشائه مكلفًا وتريد أيضًا إعادة استخدام مثيل فريد من هذا الكائن. وتجدر الإشارة إلى أنّ إنشاء مثيل لـ UserRemoteDataSource
ليس مكلفًا للغاية، لذا من غير الضروري تحديد نطاقه حسب دورة حياة المكون.
@Singleton
هو التعليق التوضيحي الوحيد على النطاق الذي يأتي مع حزمة javax.inject
. يمكنك استخدامها لإضافة تعليقات توضيحية إلى ApplicationComponent
والكائنات التي تريد إعادة استخدامها في التطبيق بأكمله.
Kotlin
@Singleton @Component(modules = [NetworkModule::class]) interface ApplicationComponent { fun inject(activity: LoginActivity) } @Singleton class UserRepository @Inject constructor( private val localDataSource: UserLocalDataSource, private val remoteDataSource: UserRemoteDataSource ) { ... } @Module class NetworkModule { // Way to scope types inside a Dagger Module @Singleton @Provides fun provideLoginRetrofitService(): LoginRetrofitService { ... } }
Java
@Singleton @Component(modules = NetworkModule.class) public interface ApplicationComponent { void inject(LoginActivity loginActivity); } @Singleton public class UserRepository { private final UserLocalDataSource userLocalDataSource; private final UserRemoteDataSource userRemoteDataSource; @Inject public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) { this.userLocalDataSource = userLocalDataSource; this.userRemoteDataSource = userRemoteDataSource; } } @Module public class NetworkModule { @Singleton @Provides public LoginRetrofitService provideLoginRetrofitService() { ... } }
يجب الحرص على عدم تسريب الذاكرة عند تطبيق النطاقات على العناصر. فما دام المكون المحدد في الذاكرة متوفرًا أيضًا، فإن الكائن الذي تم إنشاؤه موجود في الذاكرة أيضًا. بما أنّه يتم إنشاء التطبيق ApplicationComponent
عند تشغيل التطبيق (في
فئة Application
)، يتم إتلافه عندما يتم إتلافه. وبالتالي، يظل المثيل الفريد لـ UserRepository
دائمًا في الذاكرة إلى أن يتم التخلص من التطبيق.
المكوّنات الفرعية الخنجر
إذا كانت عملية تسجيل الدخول (المُدارة من خلال LoginActivity
واحد) تتألف من أجزاء متعدّدة، يجب إعادة استخدام النسخة نفسها من LoginViewModel
في جميع الأجزاء. لا يمكن لـ @Singleton
إضافة تعليق توضيحي إلى LoginViewModel
لإعادة استخدام المثيل للأسباب التالية:
سيستمر مثال
LoginViewModel
في الذاكرة بعد انتهاء التدفق.أنت تريد نسخة مختلفة من
LoginViewModel
لكل مسار تسجيل دخول. على سبيل المثال، إذا كان المستخدم يريد تسجيل خروجه، يعني ذلك أنّك تريد نسخة مختلفة منLoginViewModel
، بدلاً من النسخة نفسها التي سجَّل المستخدم الدخول فيها للمرة الأولى.
لضبط نطاق LoginViewModel
على دورة حياة LoginActivity
، عليك إنشاء
مكوِّن جديد (رسم بياني فرعي جديد) لتدفق تسجيل الدخول ونطاق جديد.
لنُنشئ رسمًا بيانيًا خاصًا بمسار تسجيل الدخول.
Kotlin
@Component interface LoginComponent {}
Java
@Component public interface LoginComponent { }
والآن، من المفترض أن يحصل LoginActivity
على إدخالات من خلال LoginComponent
لأنّه يتضمّن إعدادات خاصة بتسجيل الدخول. يؤدي هذا الإجراء إلى إزالة مسؤولية إدخال
LoginActivity
من فئة ApplicationComponent
.
Kotlin
@Component interface LoginComponent { fun inject(activity: LoginActivity) }
Java
@Component public interface LoginComponent { void inject(LoginActivity loginActivity); }
يجب أن يتمكن LoginComponent
من الوصول إلى العناصر من ApplicationComponent
لأن LoginViewModel
يعتمد على UserRepository
. الطريقة الأولى لإعلام Dagger بأنك تريد
مكون جديد يستخدم جزءًا من
مكون آخر هي باستخدام
المكوّنات الفرعية لـ Dagger يجب أن يكون المكوِّن الجديد مكونًا فرعيًا من المكون
يحتوي على موارد مشتركة.
المكوّنات الفرعية هي مكونات تكتسب الرسم البياني لكائن أحد المكوّنات الرئيسية وتوسّعه. وبالتالي، يتم توفير جميع الكائنات المتوفرة في المكون الأصلي في المكون الفرعي أيضًا. بهذه الطريقة، يمكن أن يعتمد كائن من مكون فرعي على كائن يوفره المكون الأصلي.
لإنشاء مثيلات للمكونات الفرعية، ستحتاج إلى مثيل للمكوِّن الأصلي. وبالتالي، لا تزال الكائنات التي يوفرها المكون الرئيسي إلى المكون الفرعي محددة إلى المكون الأصلي.
في المثال، يجب تحديد LoginComponent
كمكوّن فرعي من ApplicationComponent
. لتنفيذ هذا الإجراء، أضِف تعليقات توضيحية إلى LoginComponent
باستخدام السمة @Subcomponent
:
Kotlin
// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent @Subcomponent interface LoginComponent { // This tells Dagger that LoginActivity requests injection from LoginComponent // so that this subcomponent graph needs to satisfy all the dependencies of the // fields that LoginActivity is injecting fun inject(loginActivity: LoginActivity) }
Java
// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent @Subcomponent public interface LoginComponent { // This tells Dagger that LoginActivity requests injection from LoginComponent // so that this subcomponent graph needs to satisfy all the dependencies of the // fields that LoginActivity is injecting void inject(LoginActivity loginActivity); }
يجب أيضًا تحديد مصنع للمكوّنات الفرعية داخل LoginComponent
حتى يعرف
ApplicationComponent
كيفية إنشاء مثيلات LoginComponent
.
Kotlin
@Subcomponent interface LoginComponent { // Factory that is used to create instances of this subcomponent @Subcomponent.Factory interface Factory { fun create(): LoginComponent } fun inject(loginActivity: LoginActivity) }
Java
@Subcomponent public interface LoginComponent { // Factory that is used to create instances of this subcomponent @Subcomponent.Factory interface Factory { LoginComponent create(); } void inject(LoginActivity loginActivity); }
لإبلاغ Dagger بأنّ LoginComponent
هو مكوّن فرعي من
ApplicationComponent
، عليك الإشارة إليه من خلال:
إنشاء وحدة Dagger جديدة (مثل
SubcomponentsModule
) لتمرير فئة المكون الفرعي إلى السمةsubcomponents
للتعليق التوضيحيKotlin
// The "subcomponents" attribute in the @Module annotation tells Dagger what // Subcomponents are children of the Component this module is included in. @Module(subcomponents = LoginComponent::class) class SubcomponentsModule {}
Java
// The "subcomponents" attribute in the @Module annotation tells Dagger what // Subcomponents are children of the Component this module is included in. @Module(subcomponents = LoginComponent.class) public class SubcomponentsModule { }
إضافة الوحدة الجديدة (أي
SubcomponentsModule
) إلىApplicationComponent
:Kotlin
// Including SubcomponentsModule, tell ApplicationComponent that // LoginComponent is its subcomponent. @Singleton @Component(modules = [NetworkModule::class, SubcomponentsModule::class]) interface ApplicationComponent { }
Java
// Including SubcomponentsModule, tell ApplicationComponent that // LoginComponent is its subcomponent. @Singleton @Component(modules = {NetworkModule.class, SubcomponentsModule.class}) public interface ApplicationComponent { }
لن يحتاج
ApplicationComponent
إلى إدخالLoginActivity
بعد الآن لأن هذه المسؤولية أصبحت الآن ملكLoginComponent
، لذا يمكنك إزالة الطريقةinject()
منApplicationComponent
.يحتاج مستخدمو
ApplicationComponent
إلى معرفة كيفية إنشاء مثيلات منLoginComponent
. يجب أن يضيف المكوِّن الرئيسي طريقة في واجهته للسماح للمستهلكين بإنشاء نُسخ افتراضية من المكوّن الفرعي من خلال مثيل للمكوِّن الرئيسي:اعرض بيانات المصنع الذي ينشئ مثيلات
LoginComponent
في الواجهة:Kotlin
@Singleton @Component(modules = [NetworkModule::class, SubcomponentsModule::class]) interface ApplicationComponent { // This function exposes the LoginComponent Factory out of the graph so consumers // can use it to obtain new instances of LoginComponent fun loginComponent(): LoginComponent.Factory }
Java
@Singleton @Component(modules = { NetworkModule.class, SubcomponentsModule.class} ) public interface ApplicationComponent { // This function exposes the LoginComponent Factory out of the graph so consumers // can use it to obtain new instances of LoginComponent LoginComponent.Factory loginComponent(); }
تعيين النطاقات للمكونات الفرعية
وإذا أنشأت المشروع، يمكنك إنشاء مثيلات لكل من ApplicationComponent
وLoginComponent
. يتم إرفاق ApplicationComponent
بدورة حياة التطبيق لأنك تريد استخدام نفس مثيل الرسم البياني ما دام التطبيق في الذاكرة.
ما هي دورة حياة LoginComponent
؟ أحد أسباب احتياجك إلى
LoginComponent
هو أنّك كنت بحاجة إلى مشاركة النسخة نفسها من
LoginViewModel
بين الأجزاء المتعلقة بتسجيل الدخول. ولكنك تريد أيضًا حالات
مختلفة من LoginViewModel
كلما كان هناك تدفق تسجيل دخول جديد. LoginActivity
هو العمر المناسب لـ LoginComponent
: لكل نشاط جديد، تحتاج إلى
حدث جديد من LoginComponent
وأجزاء يمكن أن تستخدم مثيل LoginComponent
.
بما أنّ LoginComponent
مرتبط بدورة حياة LoginActivity
، عليك
الاحتفاظ بمرجع للمكوِّن في النشاط بالطريقة نفسها التي احتفظت بها
بالمرجع إلى applicationComponent
في فئة Application
. بهذه الطريقة، يمكن
للأجزاء الوصول إليها.
Kotlin
class LoginActivity: Activity() { // Reference to the Login graph lateinit var loginComponent: LoginComponent ... }
Java
public class LoginActivity extends Activity { // Reference to the Login graph LoginComponent loginComponent; ... }
لاحظ أن المتغير loginComponent
لا يضيف تعليقًا توضيحيًا باستخدام @Inject
لأنك لا تتوقع أن يوفر Dagger هذا المتغير.
يمكنك استخدام ApplicationComponent
للحصول على مرجع إلى LoginComponent
، ثم إدخال LoginActivity
على النحو التالي:
Kotlin
class LoginActivity: Activity() { // Reference to the Login graph lateinit var loginComponent: LoginComponent // Fields that need to be injected by the login graph @Inject lateinit var loginViewModel: LoginViewModel override fun onCreate(savedInstanceState: Bundle?) { // Creation of the login graph using the application graph loginComponent = (applicationContext as MyDaggerApplication) .appComponent.loginComponent().create() // Make Dagger instantiate @Inject fields in LoginActivity loginComponent.inject(this) // Now loginViewModel is available super.onCreate(savedInstanceState) } }
Java
public class LoginActivity extends Activity { // Reference to the Login graph LoginComponent loginComponent; // Fields that need to be injected by the login graph @Inject LoginViewModel loginViewModel; @Override protected void onCreate(Bundle savedInstanceState) { // Creation of the login graph using the application graph loginComponent = ((MyApplication) getApplicationContext()) .appComponent.loginComponent().create(); // Make Dagger instantiate @Inject fields in LoginActivity loginComponent.inject(this); // Now loginViewModel is available super.onCreate(savedInstanceState); } }
تم إنشاء LoginComponent
باستخدام طريقة onCreate()
الخاصة بالنشاط، وسيتم
تدميره بشكل ضمني عندما يتم تدمير النشاط.
يجب أن تقدّم السمة LoginComponent
دائمًا النسخة نفسها من LoginViewModel
في كل مرة يتم فيها طلبها. يمكنك ضمان ذلك عن طريق إنشاء نطاق مخصص للتعليقات التوضيحية
وإضافة تعليقات توضيحية إليهما LoginComponent
وLoginViewModel
. تجدر الإشارة
إلى أنّه لا يمكنك استخدام التعليق التوضيحي @Singleton
لأنّه سبق استخدامه
من خلال المكوّن الرئيسي، ما سيجعل العنصر تطبيقًا مفردًا
(مثال فريد للتطبيق بأكمله). تحتاج إلى إنشاء نطاق
تعليق توضيحي مختلف.
في هذه الحالة، كان من الممكن أن تسمي هذا النطاق @LoginScope
لكنها ليست
ممارسة جيدة. ينبغي ألا يكون اسم التعليق التوضيحي للنطاق
واضحًا للغرض الذي يحققه. بدلاً من ذلك، يجب تسميتها بناءً على مدة إنشائها لأنه يمكن إعادة استخدام التعليقات التوضيحية من خلال مكونات تابعة مثل RegistrationComponent
وSettingsComponent
. لهذا السبب يجب أن تسميه @ActivityScope
بدلاً من @LoginScope
.
Kotlin
// Definition of a custom scope called ActivityScope @Scope @Retention(value = AnnotationRetention.RUNTIME) annotation class ActivityScope // Classes annotated with @ActivityScope are scoped to the graph and the same // instance of that type is provided every time the type is requested. @ActivityScope @Subcomponent interface LoginComponent { ... } // A unique instance of LoginViewModel is provided in Components // annotated with @ActivityScope @ActivityScope class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... }
Java
// Definition of a custom scope called ActivityScope @Scope @Retention(RetentionPolicy.RUNTIME) public @interface ActivityScope {} // Classes annotated with @ActivityScope are scoped to the graph and the same // instance of that type is provided every time the type is requested. @ActivityScope @Subcomponent public interface LoginComponent { ... } // A unique instance of LoginViewModel is provided in Components // annotated with @ActivityScope @ActivityScope public class LoginViewModel { private final UserRepository userRepository; @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } }
الآن، إذا كان لديك جزءان يحتاجان إلى LoginViewModel
، سيتم توفير المثيل نفسه لكل منهما. على سبيل المثال، إذا كان لديك
LoginUsernameFragment
وLoginPasswordFragment
، يجب إدخالهما من خلال
LoginComponent
:
Kotlin
@ActivityScope @Subcomponent interface LoginComponent { @Subcomponent.Factory interface Factory { fun create(): LoginComponent } // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment // request injection from LoginComponent. The graph needs to satisfy // all the dependencies of the fields those classes are injecting fun inject(loginActivity: LoginActivity) fun inject(usernameFragment: LoginUsernameFragment) fun inject(passwordFragment: LoginPasswordFragment) }
Java
@ActivityScope @Subcomponent public interface LoginComponent { @Subcomponent.Factory interface Factory { LoginComponent create(); } // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment // request injection from LoginComponent. The graph needs to satisfy // all the dependencies of the fields those classes are injecting void inject(LoginActivity loginActivity); void inject(LoginUsernameFragment loginUsernameFragment); void inject(LoginPasswordFragment loginPasswordFragment); }
تصل المكونات إلى مثيل المكوِّن المتواجد في الكائن LoginActivity
. يظهر مثال الرمز LoginUserNameFragment
في مقتطف الرمز التالي:
Kotlin
class LoginUsernameFragment: Fragment() { // Fields that need to be injected by the login graph @Inject lateinit var loginViewModel: LoginViewModel override fun onAttach(context: Context) { super.onAttach(context) // Obtaining the login graph from LoginActivity and instantiate // the @Inject fields with objects from the graph (activity as LoginActivity).loginComponent.inject(this) // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
Java
public class LoginUsernameFragment extends Fragment { // Fields that need to be injected by the login graph @Inject LoginViewModel loginViewModel; @Override public void onAttach(Context context) { super.onAttach(context); // Obtaining the login graph from LoginActivity and instantiate // the @Inject fields with objects from the graph ((LoginActivity) getActivity()).loginComponent.inject(this); // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
والشيء نفسه بالنسبة إلى LoginPasswordFragment
:
Kotlin
class LoginPasswordFragment: Fragment() { // Fields that need to be injected by the login graph @Inject lateinit var loginViewModel: LoginViewModel override fun onAttach(context: Context) { super.onAttach(context) (activity as LoginActivity).loginComponent.inject(this) // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
Java
public class LoginPasswordFragment extends Fragment { // Fields that need to be injected by the login graph @Inject LoginViewModel loginViewModel; @Override public void onAttach(Context context) { super.onAttach(context); ((LoginActivity) getActivity()).loginComponent.inject(this); // Now you can access loginViewModel here and onCreateView too // (shared instance with the Activity and the other Fragment) } }
يوضّح الشكل 3 شكل الرسم البياني لـ Dagger مع المكوّن الفرعي الجديد. الصفوف التي تحتوي على نقطة بيضاء (UserRepository
وLoginRetrofitService
وLoginViewModel
) هي الفئات التي يتوفّر لها مثيل فريد على مستوى المكوّنات الخاصة بها.
لنقسم أجزاء الرسم البياني:
يتم تضمين
NetworkModule
(وبالتاليLoginRetrofitService
) فيApplicationComponent
لأنك حددته في المكوِّن.يبقى الحقل "
UserRepository
" في "ApplicationComponent
" لأنّه مخصّص لنطاقApplicationComponent
. إذا زاد المشروع، فأنت تريد مشاركة نفس المثيل عبر ميزات مختلفة (مثل التسجيل).بما أنّ
UserRepository
جزء منApplicationComponent
، يجب أن تكون تبعياته (أيUserLocalDataSource
وUserRemoteDataSource
) في هذا المكوِّن أيضًا لتتمكّن من توفير أمثلةUserRepository
.يتم تضمين
LoginViewModel
فيLoginComponent
لأنه مطلوب فقط من خلال الصفوف التي تم حقنها بواسطةLoginComponent
. لم يتم تضمينLoginViewModel
فيApplicationComponent
لأنه لا حاجة إلى الاعتمادية علىApplicationComponent
فيLoginViewModel
.وبالمثل، إذا لم تكن قد ضبطت نطاق
UserRepository
علىApplicationComponent
، كان Dagger تلقائيًا يدرجUserRepository
وتبعياته كجزء منLoginComponent
لأنّ هذا هو المكان الوحيد الذي يتم استخدامUserRepository
فيه حاليًا.
بصرف النظر عن تحديد نطاق الكائنات إلى دورة حياة مختلفة، يعتبر إنشاء مكونات فرعية ممارسة جيدة لتغليف أجزاء مختلفة من تطبيقك من بعضها البعض.
تساعد هيكلة تطبيقك لإنشاء رسوم بيانية فرعية مختلفة من Dagger بناءً على تدفق تطبيقك في استخدام تطبيق أكثر أداءً وقابلية للتوسع من حيث الذاكرة ووقت بدء التشغيل.
أفضل الممارسات عند إنشاء رسم بياني خناجر
عند إنشاء الرسم البياني لـ Dagger لتطبيقك:
عند إنشاء مكون، يجب أن تفكر في العنصر المسئول عن عمر هذا المكون. في هذه الحالة، يكون الصف
Application
مسؤولاً عنApplicationComponent
ويكونLoginActivity
هو المسؤول عنLoginComponent
.لا تستخدم تحديد النطاق إلا عندما يكون ذلك منطقيًا. يمكن أن يؤدي الإفراط في تحديد النطاق إلى تأثير سلبي على أداء وقت تشغيل التطبيق: يظل الكائن في الذاكرة طالما أن المكون موجود في الذاكرة وأن الحصول على عنصر تم تحديد نطاقه سيكون أكثر تكلفة. عندما يوفّر Dagger العنصر، يستخدم دالة الاستبعاد المتبادل
DoubleCheck
بدلاً من موفِّر نوع المصنع.
اختبار مشروع يستخدم Dagger
تتمثل إحدى فوائد استخدام أطر عمل حقن التبعية مثل Dagger في أنها تجعل اختبار التعليمة البرمجية أسهل.
اختبارات الوحدات
لا يلزمك استخدام Dagger لاختبارات الوحدات. عند اختبار فئة تستخدم حقن الدالة الإنشائية، لن تحتاج إلى استخدام Dagger لإنشاء مثيل لهذه الفئة. يمكنك أن تسميها الدالة الإنشائية التي تمرر تبعيات وهمية أو وهمية مباشرة كما تفعل إذا لم تكن هناك تعليقات توضيحية.
على سبيل المثال، عند اختبار LoginViewModel
:
Kotlin
@ActivityScope class LoginViewModel @Inject constructor( private val userRepository: UserRepository ) { ... } class LoginViewModelTest { @Test fun `Happy path`() { // You don't need Dagger to create an instance of LoginViewModel // You can pass a fake or mock UserRepository val viewModel = LoginViewModel(fakeUserRepository) assertEquals(...) } }
Java
@ActivityScope public class LoginViewModel { private final UserRepository userRepository; @Inject public LoginViewModel(UserRepository userRepository) { this.userRepository = userRepository; } } public class LoginViewModelTest { @Test public void happyPath() { // You don't need Dagger to create an instance of LoginViewModel // You can pass a fake or mock UserRepository LoginViewModel viewModel = new LoginViewModel(fakeUserRepository); assertEquals(...); } }
الاختبارات الشاملة
بالنسبة إلى اختبارات الدمج، من الممارسات الجيدة إنشاء
TestApplicationComponent
مخصصة للاختبار.
يستخدم الإنتاج والاختبار إعدادات مختلفة للمكوِّن.
يتطلّب هذا تصميمًا أكثر تفصيلاً للوحدات في تطبيقك. يقوم مكون الاختبار بتوسيع مكون الإنتاج وتثبيت مجموعة مختلفة من الوحدات.
Kotlin
// TestApplicationComponent extends from ApplicationComponent to have them both // with the same interface methods. You need to include the modules of the // component here as well, and you can replace the ones you want to override. // This sample uses FakeNetworkModule instead of NetworkModule @Singleton @Component(modules = [FakeNetworkModule::class, SubcomponentsModule::class]) interface TestApplicationComponent : ApplicationComponent { }
Java
// TestApplicationComponent extends from ApplicationComponent to have them both // with the same interface methods. You need to include the modules of the // Component here as well, and you can replace the ones you want to override. // This sample uses FakeNetworkModule instead of NetworkModule @Singleton @Component(modules = {FakeNetworkModule.class, SubcomponentsModule.class}) public interface TestApplicationComponent extends ApplicationComponent { }
تتضمّن FakeNetworkModule
عملية تنفيذ وهمية للسمة NetworkModule
الأصلية.
حيث يمكنك تقديم نسخ مزيفة أو نماذج لأي شيء تريد استبداله.
Kotlin
// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService // that you can use in your tests. @Module class FakeNetworkModule { @Provides fun provideLoginRetrofitService(): LoginRetrofitService { return FakeLoginService() } }
Java
// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService // that you can use in your tests. @Module public class FakeNetworkModule { @Provides public LoginRetrofitService provideLoginRetrofitService() { return new FakeLoginService(); } }
في اختبارات الدمج أو الاختبارات الشاملة، استخدِم TestApplication
لإنشاء TestApplicationComponent
بدلاً من ApplicationComponent
.
Kotlin
// Your test application needs an instance of the test graph class MyTestApplication: MyApplication() { override val appComponent = DaggerTestApplicationComponent.create() }
Java
// Your test application needs an instance of the test graph public class MyTestApplication extends MyApplication { ApplicationComponent appComponent = DaggerTestApplicationComponent.create(); }
بعد ذلك، يتمّ استخدام تطبيق الاختبار هذا في TestRunner
مخصّصة ستستخدمها لإجراء اختبارات الأدوات. للحصول على مزيد من المعلومات حول هذا الموضوع، يمكنك الاطّلاع على استخدام مؤشر الخنق في الدرس التطبيقي حول ترميز تطبيق Android.
التعامل مع وحدات Dagger
تشكّل وحدات الخناجر طريقة لتغليف طريقة تقديم الكائنات بطريقة دلالية. يمكنك تضمين وحدات في المكوّنات ولكن يمكنك أيضًا تضمين وحدات داخل الوحدات الأخرى. وهذه الميزة فعّالة، ولكن يمكن إساءة استخدامها بسهولة.
بمجرد إضافة وحدة إلى مكون أو وحدة أخرى، تكون موجودة بالفعل في الرسم البياني لـ Dagger؛ يمكن لـ Dagger توفير هذه الكائنات في هذا المكون. قبل إضافة وحدة، تحقق مما إذا كانت تلك الوحدة جزءًا من الرسم البياني لـ Dagger بالفعل عن طريق التحقق مما إذا كانت قد تمت إضافتها بالفعل إلى المكون أو من خلال تجميع المشروع ومعرفة ما إذا كان بإمكان Dagger العثور على التبعيات المطلوبة لتلك الوحدة أم لا.
تشير الممارسة الجيدة إلى أنّه يجب الإعلان عن الوحدات مرة واحدة فقط في المكوّن (خارج إطار حالات استخدام Dagger المتقدّمة).
لنفترض أنه قد تم تكوين الرسم البياني بهذه الطريقة. ApplicationComponent
تشمل Module1
وModule2
وModule1
تشمل ModuleX
.
Kotlin
@Component(modules = [Module1::class, Module2::class]) interface ApplicationComponent { ... } @Module(includes = [ModuleX::class]) class Module1 { ... } @Module class Module2 { ... }
Java
@Component(modules = {Module1.class, Module2.class}) public interface ApplicationComponent { ... } @Module(includes = {ModuleX.class}) public class Module1 { ... } @Module public class Module2 { ... }
إذا كان Module2
يعتمد الآن على الصفوف التي يوفّرها ModuleX
. من الممارسة السيئة
إدراج ModuleX
في Module2
لأنه تم تضمين ModuleX
مرتين في
الرسم البياني كما هو موضح في مقتطف الرمز التالي:
Kotlin
// Bad practice: ModuleX is declared multiple times in this Dagger graph @Component(modules = [Module1::class, Module2::class]) interface ApplicationComponent { ... } @Module(includes = [ModuleX::class]) class Module1 { ... } @Module(includes = [ModuleX::class]) class Module2 { ... }
Java
// Bad practice: ModuleX is declared multiple times in this Dagger graph. @Component(modules = {Module1.class, Module2.class}) public interface ApplicationComponent { ... } @Module(includes = ModuleX.class) public class Module1 { ... } @Module(includes = ModuleX.class) public class Module2 { ... }
وبدلاً من ذلك، يمكنك تنفيذ أحد الإجراءات التالية:
- أعِد هيكلة الوحدات السكنية واستخرِج الوحدة المشتركة من المكوِّن.
- يمكنك إنشاء وحدة جديدة باستخدام العناصر التي تشترك فيها كلتا الوحدتين واستخراجها إلى المكوّن.
ينتج عن عدم إعادة البناء بهذه الطريقة العديد من الوحدات التي تشمل بعضها البعض بدون إحساس واضح بالتنظيم ويجعل من الصعب معرفة مصدر كل تبعية.
الممارسة الجيدة (الخيار 1): يتم الإعلان عن الوحدة X مرة واحدة في الرسم البياني لـ Dagger.
Kotlin
@Component(modules = [Module1::class, Module2::class, ModuleX::class]) interface ApplicationComponent { ... } @Module class Module1 { ... } @Module class Module2 { ... }
Java
@Component(modules = {Module1.class, Module2.class, ModuleX.class}) public interface ApplicationComponent { ... } @Module public class Module1 { ... } @Module public class Module2 { ... }
الممارسة الجيدة (الخيار 2): يتم استخراج التبعيات الشائعة من Module1
وModule2
في ModuleX
إلى وحدة جديدة باسم ModuleXCommon
يتم تضمينها في المكوِّن. بعد ذلك، يتم إنشاء وحدتَين أخريَين باسم
ModuleXWithModule1Dependencies
وModuleXWithModule2Dependencies
باستخدام التبعيات الخاصة بكل وحدة. يتم تعريف جميع الوحدات
مرة واحدة في الرسم البياني لـ Dagger.
Kotlin
@Component(modules = [Module1::class, Module2::class, ModuleXCommon::class]) interface ApplicationComponent { ... } @Module class ModuleXCommon { ... } @Module class ModuleXWithModule1SpecificDependencies { ... } @Module class ModuleXWithModule2SpecificDependencies { ... } @Module(includes = [ModuleXWithModule1SpecificDependencies::class]) class Module1 { ... } @Module(includes = [ModuleXWithModule2SpecificDependencies::class]) class Module2 { ... }
Java
@Component(modules = {Module1.class, Module2.class, ModuleXCommon.class}) public interface ApplicationComponent { ... } @Module public class ModuleXCommon { ... } @Module public class ModuleXWithModule1SpecificDependencies { ... } @Module public class ModuleXWithModule2SpecificDependencies { ... } @Module(includes = ModuleXWithModule1SpecificDependencies.class) public class Module1 { ... } @Module(includes = ModuleXWithModule2SpecificDependencies.class) public class Module2 { ... }
مساعد الحقن
يُقصد بحقن الإدخال المساعد نمط DI الذي يُستخدَم لإنشاء عنصر حيث يمكن توفير بعض المعلَمات من خلال إطار عمل DI ويجب تمرير البعض الآخر عند وقت الإنشاء من قِبل المستخدم.
في Android، يكون هذا النمط شائعًا في شاشات التفاصيل حيث يكون معرّف العنصر المطلوب عرضه معروفًا فقط في وقت التشغيل، وليس في وقت التجميع عندما ينشئ Dagger الرسم البياني لـ DI. لمعرفة المزيد من المعلومات حول الحقن المدعوم باستخدام Dagger، راجِع مستندات Dagger.
الخاتمة
راجِع قسم أفضل الممارسات إذا لم تكن قد فعلت ذلك بعد. لمعرفة كيفية استخدام Dagger في تطبيق Android، يُرجى الاطّلاع على استخدام Dagger في الدرس التطبيقي حول ترميز تطبيق Android.