מידע בסיסי על Dagger

החדרה ידנית של תלות או שירות שירותי האיתור באפליקציה ל-Android עלולים להיות בעייתיים, בהתאם לגודל פרויקט. אפשר להגביל את מורכבות הפרויקט ככל שהוא יתרחב באמצעות Dagger לניהול יחסי התלות.

Dagger יוצר באופן אוטומטי קוד שמחקה את הקוד שאחרת בכתב יד. מאחר שהקוד נוצר בזמן הידור (compile), ניתן לעקוב אחריו. והם בעלי ביצועים טובים יותר מאשר פתרונות אחרים שמבוססים על השתקפות, Guice.

יתרונות השימוש ב-Dagger

Dagger מאפשר לכם לכתוב קוד מסובך ומעורפל יותר מפני שגיאות על ידי:

  • יצירת הקוד של AppContainer (תרשים האפליקציות) שיצרת באופן ידני הטמענו בקטע ה-DI הידני.

  • יצירת מפעלים עבור הכיתות הזמינות בתרשים האפליקציה. הזה הוא האופן שבו יחסי התלות מתקיימים באופן פנימי.

  • להחליט אם להשתמש שוב בתלות או ליצור מכונה חדשה באמצעות שימוש בהיקפים.

  • יצירת קונטיינרים לתהליכים ספציפיים כפי שיצרתם בתהליך ההתחברות את הקטע הקודם באמצעות רכיבי משנה של Dagger. זה משפר את הביצועים של האפליקציה על ידי שחרור אובייקטים בזיכרון כשאין בהם יותר צורך.

Dagger עושה את כל זה באופן אוטומטי בזמן ה-build, כל עוד להצהיר על יחסי תלות של מחלקה ולציין איך לספק אותם באמצעות הערות. Dagger יוצר קוד שדומה למה שהיית כותב במצב ה-GRU באופן פנימי, Dagger יוצר גרף של אובייקטים שהוא יכול להפנות אליהם כדי למצוא את הדרך לספק מופע של כיתה. לגבי כל כיתה בתרשים, Dagger יוצר מחלקה של סוג מפעל שבה הוא משתמש באופן פנימי כדי לקבל מופעים מהסוג הזה.

בזמן ה-build, 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 ל-constructor של 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 constructor.

  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 לעשות את זה, צריך ליצור ממשק ולהוסיף לו הערות @Component Dagger יוצר קונטיינר כמו שהיית עושה ידנית החדרת תלות.

בתוך הממשק של @Component אפשר להגדיר פונקציות שמחזירות של הכיתות הנדרשות (למשל UserRepository). @Component אומרת צלבון שמייצר קונטיינר עם כל יחסי התלות שדרושים כדי לעמוד הסוגים שהיא חושפת. רכיב זה נקרא רכיב 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 ל-constructor של המחלקות ViewModel; אבל ב-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.