استخدام Dagger في تطبيقات Android

أوضحت صفحة أساسيات 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.

يعتمد نشاط تسجيل الدخول على LoginViewModel الذي يعتمد على UserRepository، الذي يعتمد على UserLocalDataSource وUserRemoteDataSource، اللذين
 يعتمدان بدورهما على Retrofit.

الشكل 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 في المثال الآن:

مخطط للرسم البياني لتبعية تسجيل الدخول

الشكل 2. تمثيل الرسم البياني مع إضافة LoginActivity باستخدام 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 لإعادة استخدام المثيل للأسباب التالية:

  1. سيستمر مثال LoginViewModel في الذاكرة بعد انتهاء التدفق.

  2. أنت تريد نسخة مختلفة من 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، عليك الإشارة إليه من خلال:

  1. إنشاء وحدة 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 {
    }
    
  2. إضافة الوحدة الجديدة (أي 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. يجب أن يضيف المكوِّن الرئيسي طريقة في واجهته للسماح للمستهلكين بإنشاء نُسخ افتراضية من المكوّن الفرعي من خلال مثيل للمكوِّن الرئيسي:

  3. اعرض بيانات المصنع الذي ينشئ مثيلات 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) هي الفئات التي يتوفّر لها مثيل فريد على مستوى المكوّنات الخاصة بها.

الرسم البياني للتطبيق بعد إضافة المكوّن الفرعي الأخير

الشكل 3. تمثيل للرسم البياني الذي أنشأته لمثال تطبيق Android

لنقسم أجزاء الرسم البياني:

  1. يتم تضمين NetworkModule (وبالتالي LoginRetrofitService) في ApplicationComponent لأنك حددته في المكوِّن.

  2. يبقى الحقل "UserRepository" في "ApplicationComponent" لأنّه مخصّص لنطاق ApplicationComponent. إذا زاد المشروع، فأنت تريد مشاركة نفس المثيل عبر ميزات مختلفة (مثل التسجيل).

    بما أنّ UserRepository جزء من ApplicationComponent، يجب أن تكون تبعياته (أي UserLocalDataSource وUserRemoteDataSource) في هذا المكوِّن أيضًا لتتمكّن من توفير أمثلة UserRepository.

  3. يتم تضمين 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. أعِد هيكلة الوحدات السكنية واستخرِج الوحدة المشتركة من المكوِّن.
  2. يمكنك إنشاء وحدة جديدة باستخدام العناصر التي تشترك فيها كلتا الوحدتين واستخراجها إلى المكوّن.

ينتج عن عدم إعادة البناء بهذه الطريقة العديد من الوحدات التي تشمل بعضها البعض بدون إحساس واضح بالتنظيم ويجعل من الصعب معرفة مصدر كل تبعية.

الممارسة الجيدة (الخيار 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.