أساسيات الخناجر

قد يمثل تضمين التبعية اليدوي أو محددات مواقع الخدمة في تطبيق Android مشكلة بناءً على حجم مشروعك. يمكنك الحدّ من مدى تعقيد مشروعك لأنّه يتزايد باستخدام Dagger لإدارة التبعيات.

ينشئ Dagger تلقائيًا رمزًا برمجيًا يحاكي الرمز الذي كنت ستكتبه يدويًا. ونظرًا لأن الرمز يتم إنشاؤه في وقت التجميع، يمكن تعقّبه ويكون أكثر فعالية من الحلول الأخرى القائمة على الانعكاس مثل Guice.

مزايا استخدام Dagger

يوفر لك Dagger إمكانية كتابة تعليمات برمجية نموذجية مملة وعرضة للأخطاء عن طريق:

  • جارٍ إنشاء رمز AppContainer (الرسم البياني للتطبيق) الذي نفّذته يدويًا في قسم DI اليدوي.

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

  • تحديد ما إذا كنت تريد إعادة استخدام التبعية أو إنشاء مثيل جديد من خلال استخدام النطاقات

  • إنشاء حاويات لتدفقات معينة كما فعلت مع تدفق تسجيل الدخول في القسم السابق باستخدام المكونات الفرعية لـ Dagger. ويؤدي ذلك إلى تحسين أداء تطبيقك عن طريق إطلاق عناصر في الذاكرة عند عدم الحاجة إليها.

ينفذ Dagger كل هذا تلقائيًا في وقت الإنشاء طالما أنك تعلن عن تبعيات لفئة معينة وتحدد كيفية تلبيتها باستخدام التعليقات التوضيحية. ينشئ Dagger تعليمات برمجية مشابهة لما كنت تكتبه يدويًا. داخليًا، ينشئ Dagger رسمًا بيانيًا للكائنات التي يمكن أن تشير إلى طريقة لتوفير مثيل من الفئة. لكل فئة في الرسم البياني، ينشئ Dagger فئة factory-type يستخدمها داخليًا للحصول على مثيلات من هذا النوع.

في وقت الإصدار، يتصفح Dagger التعليمات البرمجية الخاصة بك و:

  • تنشئ الرسوم البيانية للتبعية وتتحقّق من صحتها، لضمان ما يلي:

    • يمكن استيفاء تبعيات كل كائن، لذلك لا توجد استثناءات في وقت التشغيل.
    • لا توجد دورات تبعية، لذلك لا توجد حلقات لانهائية.
  • تنشئ الفئات المستخدمة في وقت التشغيل لإنشاء الكائنات الفعلية وتبعياتها.

حالة استخدام بسيطة في Dagger: إنشاء مصنع

لتوضيح كيف يمكنك العمل مع Dagger، لننشئ مصنعًا بسيطًا لفئة UserRepository كما هو موضح في المخطط التالي:

حدِّد UserRepository على النحو التالي:

Kotlin

class UserRepository(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }

    ...
}

أضِف تعليق توضيحي من @Inject إلى الدالة الإنشائية UserRepository حتى يعرف Dagger كيفية إنشاء UserRepository:

Kotlin

// @Inject lets Dagger know how to create instances of this object
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    // @Inject lets Dagger know how to create instances of this object
    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

في مقتطف الرمز أعلاه، أنت تخبر Dagger:

  1. كيفية إنشاء مثيل UserRepository باستخدام الدالة الإنشائية @Inject ذات التعليقات التوضيحية.

  2. ما هي تبعياته: UserLocalDataSource وUserRemoteDataSource.

يعرف Dagger الآن كيفية إنشاء مثيل UserRepository، لكنه لا يعرف كيفية إنشاء تبعياته. إذا قمت بإضافة تعليقات توضيحية إلى الفئات الأخرى أيضًا، فإن Dagger يعرف كيفية إنشائها:

Kotlin

// @Inject lets Dagger know how to create instances of these objects
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }

Java

public class UserLocalDataSource {
    @Inject
    public UserLocalDataSource() { }
}

public class UserRemoteDataSource {
    @Inject
    public UserRemoteDataSource() { }
}

مكوّنات الخناجر

يمكن لـ Dagger إنشاء رسم بياني للتبعيات في مشروعك الذي يمكنه استخدامه لمعرفة أين يجب أن يحصل على تلك التبعيات عندما تكون هناك حاجة إليها. لتنفيذ هذا الإجراء في Dagger، يجب إنشاء واجهة وإضافة تعليقات توضيحية إليها باستخدام @Component. ينشئ Dagger حاوية كما كنت تفعل مع حقن التبعية اليدوي.

داخل واجهة @Component، يمكنك تحديد الدوال التي تعرض نسخًا افتراضية من الفئات التي تحتاج إليها (مثل UserRepository). يطلب @Component من Dagger إنشاء حاوية تتضمّن جميع التبعيات المطلوبة لاستيفاء الأنواع التي تعرضها. يسمى هذا مكون الخنجر؛ وهو يحتوي على رسم بياني يتكون من الكائنات التي يعرف Dagger كيفية تقديمها وتبعياتها الخاصة.

Kotlin

// @Component makes Dagger create a graph of dependencies
@Component
interface ApplicationGraph {
    // The return type  of functions inside the component interface is
    // what can be provided from the container
    fun repository(): UserRepository
}

Java

// @Component makes Dagger create a graph of dependencies
@Component
public interface ApplicationGraph {
    // The return type  of functions inside the component interface is
    // what can be consumed from the graph
    UserRepository userRepository();
}

عند إنشاء المشروع، ينشئ Dagger عملية تنفيذ واجهة ApplicationGraph لك: DaggerApplicationGraph. باستخدام معالج التعليقات التوضيحية، ينشئ Dagger رسمًا بيانيًا للتبعية يتألف من العلاقات بين الفئات الثلاث (UserRepository وUserLocalDatasource وUserRemoteDataSource) مع نقطة دخول واحدة فقط: الحصول على مثيل UserRepository. يمكنك استخدامه على النحو التالي:

Kotlin

// Create an instance of the application graph
val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()
// Grab an instance of UserRepository from the application graph
val userRepository: UserRepository = applicationGraph.repository()

Java

// Create an instance of the application graph
ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

// Grab an instance of UserRepository from the application graph
UserRepository userRepository = applicationGraph.userRepository();

ينشئ Dagger مثيلاً جديدًا من UserRepository في كل مرة يتم فيها طلبها.

Kotlin

val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository != userRepository2)

Java

ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

UserRepository userRepository = applicationGraph.userRepository();
UserRepository userRepository2 = applicationGraph.userRepository();

assert(userRepository != userRepository2)

أحيانًا تحتاج إلى وجود مثيل فريد للتبعية في الحاوية. قد ترغب في ذلك لعدة أسباب:

  1. تريد أن تشارك الأنواع الأخرى من هذا النوع كتبعية المثيل نفسه، مثل كائنات ViewModel متعددة في مسار تسجيل الدخول باستخدام السمة LoginUserData نفسها.

  2. إنشاء الكائن مكلف ولا تريد إنشاء مثيل جديد في كل مرة يتم تعريفه فيها كتبعية (على سبيل المثال، محلل JSON).

في المثال، قد ترغب في الحصول على مثيل فريد من UserRepository في الرسم البياني بحيث تحصل دائمًا على الحالة نفسها في كل مرة تطلب فيها UserRepository. ويُعدّ ذلك مفيدًا في المثال الذي تقدّمه لأنّه في تطبيق واقعي مع رسم بياني أكثر تعقيدًا، قد يكون لديك عدة كائنات ViewModel بناءً على UserRepository ولا تريد إنشاء مثيلات جديدة من UserLocalDataSource وUserRemoteDataSource في كل مرة يجب فيها تقديم UserRepository.

من خلال حقن التبعية اليدوي، يتم تنفيذ ذلك من خلال تمرير مثيل UserRepository نفسه إلى التركيبات البرمجية لفئات ViewModel، ولكن في "Dagger"، نظرًا لأنك لا تكتب هذا الرمز يدويًا، عليك إعلام Dagger بأنّك تريد استخدام الحالة نفسها. يمكن القيام بذلك باستخدام التعليقات التوضيحية على النطاق.

تحديد النطاق باستخدام Dagger

يمكنك استخدام التعليقات التوضيحية للنطاق لحصر عمر العنصر على فترة بقاء مكوّنه. هذا يعني أنه يتم استخدام نفس مثيل التبعية في كل مرة يلزم فيها تقديم هذا النوع.

للحصول على مثيل فريد من UserRepository عند طلب المستودع في ApplicationGraph، يمكنك استخدام التعليق التوضيحي على النطاق نفسه للواجهة @Component وUserRepository. يمكنك استخدام التعليق التوضيحي @Singleton الذي يتضمّن حزمة javax.inject التي يستخدمها Dagger:

Kotlin

// Scope annotations on a @Component interface informs Dagger that classes annotated
// with this annotation (i.e. @Singleton) are bound to the life of the graph and so
// the same instance of that type is provided every time the type is requested.
@Singleton
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

// Scope this class to a component using @Singleton scope (i.e. ApplicationGraph)
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

Java

// Scope annotations on a @Component interface informs Dagger that classes annotated
// with this annotation (i.e. @Singleton) are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@Singleton
@Component
public interface ApplicationGraph {
    UserRepository userRepository();
}

// Scope this class to a component using @Singleton scope (i.e. ApplicationGraph)
@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;
    }
}

بدلاً من ذلك، يمكنك إنشاء واستخدام تعليق توضيحي على نطاق مخصّص. يمكنك إنشاء تعليق توضيحي للنطاق على النحو التالي:

Kotlin

// Creates MyCustomScope
@Scope
@MustBeDocumented
@Retention(value = AnnotationRetention.RUNTIME)
annotation class MyCustomScope

Java

// Creates MyCustomScope
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface MyCustomScope {}

وبعد ذلك، يمكنك استخدامه على النحو السابق:

Kotlin

@MyCustomScope
@Component
interface ApplicationGraph {
    fun repository(): UserRepository
}

@MyCustomScope
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val service: UserService
) { ... }

Java

@MyCustomScope
@Component
public interface ApplicationGraph {
    UserRepository userRepository();
}

@MyCustomScope
public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

وفي كلتا الحالتين، يتم توفير الكائن باستخدام النطاق نفسه المستخدَم لإضافة تعليقات توضيحية إلى واجهة @Component. وبالتالي، في كل مرة يتم فيها الاتصال بـ applicationGraph.repository()، ستحصل على مثيل UserRepository نفسه.

Kotlin

val applicationGraph: ApplicationGraph = DaggerApplicationGraph.create()

val userRepository: UserRepository = applicationGraph.repository()
val userRepository2: UserRepository = applicationGraph.repository()

assert(userRepository == userRepository2)

Java

ApplicationGraph applicationGraph = DaggerApplicationGraph.create();

UserRepository userRepository = applicationGraph.userRepository();
UserRepository userRepository2 = applicationGraph.userRepository();

assert(userRepository == userRepository2)

الخاتمة

من المهم أن تكون على دراية بفوائد Dagger وأساسيات كيفية عملها قبل أن تتمكن من استخدامها في سيناريوهات أكثر تعقيدًا.

في الصفحة التالية، ستتعرّف على كيفية إضافة Dagger إلى تطبيق Android.