استخدام 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 اللذين
  يعتمد على عملية التحديث.

الشكل 1. الرسم البياني التبعية للمثال رقم الاعتماد

في نظام Android، يمكنك عادةً إنشاء رسم بياني الخنجر المتوفّر في تطبيقك. لأنك تريد أن يكون هناك مثيل للرسم البياني في الذاكرة طالما تطبيقك قيد التشغيل. بهذه الطريقة، يتم إرفاق الرسم البياني بدورة حياة التطبيق. في بعض من الحالات، قد ترغب أيضًا في توفير سياق التطبيق في الرسم البياني. ولإجراء ذلك، يجب أيضًا أن يكون الرسم البياني في صف واحد (Application). من مزايا ذلك هو أن يكون الرسم البياني متوفرًا لفئات إطار عمل Android الأخرى. بالإضافة إلى ذلك، يبسّط الاختبار من خلال السماح لك باستخدام صف واحد (Application) في الاختبارات.

ولأنّ الواجهة التي تُنشئ الرسم البياني تتضمّن تعليقات توضيحية باستخدام @Component، يمكنك تسميته ApplicationComponent أو ApplicationGraph. عادةً ما تحتفظ تمثّل هذه السمة مثيلاً لهذا المكوِّن في فئة Application المخصّصة وتسمّيه في كل مرة تحتاج فيها إلى الرسم البياني للتطبيق، كما هو موضح في الرمز التالي snippet:

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 الهندسية. ViewModel؛ إنها مجرد فئة عادية تعمل كـ ViewModel. لمزيد من المعلومات حول كيفية إدخال هذه الفئات، راجع الرمز في تنفيذ Dagger من Android Blueprints الرسمي، في فرع 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;
    }
}

وحدات Dagger

في هذا المثال، أنت تستخدم مكتبة الشبكات إعادة التعديل. تعتمد الدالة UserRemoteDataSource على LoginRetrofitService. ومع ذلك، تختلف طريقة إنشاء مثيل LoginRetrofitService عن التي كنت تفعلها حتى الآن. إنه ليس مثيلاً للفئة؛ إنها نتيجة استدعاء Retrofit.Builder() وتمرير معلَمات مختلفة لضبطها خدمة تسجيل الدخول.

بخلاف التعليق التوضيحي @Inject، هناك طريقة أخرى لإخبار 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

تم ذكر النطاقات في صفحة أساسيات 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 في الذاكرة دائمًا حتى تم إتلاف التطبيق.

مكوّنات Dagger الفرعية

إذا كان مسار تسجيل الدخول (الذي يديره 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 كيف يبدو الرسم البياني الخنجر مع المكون الفرعي الجديد. الصفوف مع نقطة بيضاء (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

عند إنشاء رسم 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 مخصّص ستستخدمه. إجراء اختبارات الأجهزة. لمزيد من المعلومات حول هذا الأمر، اطلع على استخدام Dagger في الدرس التطبيقي حول ترميز تطبيق Android.

استخدام وحدات Dagger

تشكّل وحدات 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): يتم تعريف ModuleX مرة واحدة في الرسم البياني لـ 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. درس تطبيقي حول الترميز.