שימוש ב-Dagger באפליקציות עם מודולים מרובים

פרויקט עם כמה מודולים של Gradle נקרא 'פרויקט עם מודולים מרובים'. בפרויקט עם מודולים מרובים שנשלח כ-APK יחיד ללא תכונה לעיתים קרובות יש מודול app שיכול להיות תלוי של הפרויקט ומודול base או core, הם תלויים בדרך כלל. המודול app מכיל בדרך כלל את הכיתה Application, ואילו base מכיל את כל המחלקות המשותפות שמשותפות בכל המודולים בפרויקט.

המודול app הוא מקום טוב להצהיר עליו על רכיב האפליקציה (עבור למשל, ApplicationComponent בתמונה שלמטה) שיכול לספק אובייקטים שרכיבים אחרים עשויים להזדקק להם, וכן יחידות הסינגלונים של האפליקציה. בתור למשל, מחלקות כמו OkHttpClient, כלי ניתוח JSON, כלי גישה למסד הנתונים שלך, או SharedPreferences אובייקטים שעשויים להיות מוגדרים במודול core, יסופק על ידי ה-ApplicationComponent שהוגדר במודול app.

במודול app יכולים להיות גם רכיבים אחרים עם תוחלת חיים קצרה יותר. לדוגמה: UserComponent עם הגדרות ספציפיות למשתמש (כמו UserSession) אחרי התחברות.

במודולים השונים של הפרויקט, אפשר להגדיר לפחות אחד רכיב משנה שיש לו לוגיקה ספציפית למודול הזה, כפי שמוצג באיור 1.

איור 1. דוגמה לתרשים של צלבון פרויקט עם מודולים מרובים

לדוגמה, במודול login, יכול להיות שיש LoginComponent בהיקף עם הערה מותאמת אישית מסוג @ModuleScope שיכולה לספק אובייקטים משותפים לאותה תכונה, כמו LoginRepository. בתוך המודול הזה, תוכלו גם יש רכיבים אחרים שתלויים ב-LoginComponent עם ערך מותאם אישית אחר לדוגמה, @FeatureScope עבור LoginActivityComponent או TermsAndConditionsComponent, עם אפשרות להיקף לוגיקה יותר ספציפית לתכונות כמו ViewModel אובייקטים.

במודולים אחרים, כמו Registration, תראו הגדרה דומה.

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

באופן כללי, מומלץ ליצור רכיב במקרים הבאים:

  • צריך לבצע החדרת שדה, כמו ב-LoginActivityComponent.

  • צריך להגדיר את היקף האובייקטים, כמו ב-LoginComponent.

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

הטמעה באמצעות רכיבי המשנה של Dagger

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

Kotlin

class LoginActivity: Activity() {
  ...

  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)
    ...
  }
}

Java

public class LoginActivity extends Activity {
    ...

    @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);

        ...
    }
}

הסיבה לכך היא שהמודול login לא יודע על MyApplication או appComponent. כדי שהיא תפעל, עליך להגדיר ממשק בתכונה המודול שמספק FeatureComponent שנדרש ל-MyApplication ליישם.

בדוגמה הבאה אפשר להגדיר ממשק של LoginComponentProvider. שמספק LoginComponent במודול login לתהליך ההתחברות:

Kotlin

interface LoginComponentProvider {
    fun provideLoginComponent(): LoginComponent
}

Java

public interface LoginComponentProvider {
   public LoginComponent provideLoginComponent();
}

עכשיו, LoginActivity ישתמש בממשק הזה במקום בקטע הקוד מוגדר למעלה:

Kotlin

class LoginActivity: Activity() {
  ...

  override fun onCreate(savedInstanceState: Bundle?) {
    loginComponent = (applicationContext as LoginComponentProvider)
                        .provideLoginComponent()

    loginComponent.inject(this)
    ...
  }
}

Java

public class LoginActivity extends Activity {
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        loginComponent = ((LoginComponentProvider) getApplicationContext())
                                .provideLoginComponent();

        loginComponent.inject(this);

        ...
    }
}

עכשיו MyApplication צריך להטמיע את הממשק ולהטמיע את השיטות הנדרשות:

Kotlin

class MyApplication: Application(), LoginComponentProvider {
  // Reference to the application graph that is used across the whole app
  val appComponent = DaggerApplicationComponent.create()

  override fun provideLoginComponent(): LoginComponent {
    return appComponent.loginComponent().create()
  }
}

Java

public class MyApplication extends Application implements LoginComponentProvider {
  // Reference to the application graph that is used across the whole app
  ApplicationComponent appComponent = DaggerApplicationComponent.create();

  @Override
  public LoginComponent provideLoginComponent() {
    return appComponent.loginComponent.create();
  }
}

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

יחסי תלות של רכיבים עם מודולים של תכונות

במודולים של תכונות, האופן שבו המודולים תלויים בדרך כלל הפוך. במקום המודול app כולל את התכונה מודולים, המודולים של התכונות תלויים במודול app. ראו איור 2 לייצוג של המבנה של המודולים.

איור 2. דוגמה לתרשים של צלבון פרויקט עם מודולים של מאפיינים

ב-Dagger, הרכיבים צריכים לדעת על רכיבי המשנה שלהם. המידע הזה נכלל במודול Dagger שנוסף לרכיב ההורה (למשל מודול SubcomponentsModule בשימוש ב-Dagger באפליקציות ל-Android).

לצערי, המשמעות של היפוך יחסי בין האפליקציה מודול המאפיין, רכיב המשנה לא גלוי מהמודול app כי הוא לא בנתיב ה-build. לדוגמה, LoginComponent מוגדר מודול התכונה login לא יכול להיות רכיב משנה של השדה ApplicationComponent הוגדר במודול app.

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

לדוגמה: מודול של תכונה בשם login רוצה ליצור LoginComponent שתלויה ב-AppComponent שזמינים app מודול Gradle.

בהמשך מוצגות ההגדרות של הכיתות ושל AppComponent שהן חלק מ- מודול Gradle app:

Kotlin

// UserRepository's dependencies
class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor() { ... }

// UserRepository is scoped to AppComponent
@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

@Singleton
@Component
interface AppComponent { ... }

Java

// UserRepository's dependencies
public class UserLocalDataSource {

    @Inject
    public UserLocalDataSource() {}
}

public class UserRemoteDataSource {

    @Inject
    public UserRemoteDataSource() { }
}

// UserRepository is scoped to AppComponent
@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;
    }
}

@Singleton
@Component
public interface ApplicationComponent { ... }

במודול login (GRid) שכולל את מודול המסלול app, יש LoginActivity שנדרשת כדי להחדיר מופע LoginViewModel:

Kotlin

// LoginViewModel depends on UserRepository that is scoped to AppComponent
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

// LoginViewModel depends on UserRepository that is scoped to AppComponent
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

ל-LoginViewModel יש תלות ב-UserRepository הזמינה ו בהיקף של AppComponent. בואו ניצור LoginComponent שמבוסס על AppComponent כדי להחדיר LoginActivity:

Kotlin

// Use the dependencies attribute in the Component annotation to specify the
// dependencies of this Component
@Component(dependencies = [AppComponent::class])
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

Java

// Use the dependencies attribute in the Component annotation to specify the
// dependencies of this Component
@Component(dependencies = AppComponent.class)
public interface LoginComponent {

    void inject(LoginActivity loginActivity);
}

LoginComponent מציין תלות ב-AppComponent על ידי הוספתה אל של נתוני התלות של ההערה של הרכיב. כי LoginActivity להחדיר את Dagger, מוסיפים את השיטה inject() לממשק.

כשיוצרים LoginComponent, מופע של AppComponent צריך להיות הועברה. לשם כך, יש להשתמש ביצרן הרכיב:

Kotlin

@Component(dependencies = [AppComponent::class])
interface LoginComponent {

    @Component.Factory
    interface Factory {
        // Takes an instance of AppComponent when creating
        // an instance of LoginComponent
        fun create(appComponent: AppComponent): LoginComponent
    }

    fun inject(activity: LoginActivity)
}

Java

@Component(dependencies = AppComponent.class)
public interface LoginComponent {

    @Component.Factory
    interface Factory {
        // Takes an instance of AppComponent when creating
        // an instance of LoginComponent
        LoginComponent create(AppComponent appComponent);
    }

    void inject(LoginActivity loginActivity);
}

עכשיו, LoginActivity יכול ליצור מופע של LoginComponent ולקרוא אמצעי תשלום אחד (inject()).

Kotlin

class LoginActivity: Activity() {

    // You want Dagger to provide an instance of LoginViewModel from the Login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Gets appComponent from MyApplication available in the base Gradle module
        val appComponent = (applicationContext as MyApplication).appComponent

        // Creates a new instance of LoginComponent
        // Injects the component to populate the @Inject fields
        DaggerLoginComponent.factory().create(appComponent).inject(this)

        super.onCreate(savedInstanceState)

        // Now you can access loginViewModel
    }
}

Java

public class LoginActivity extends Activity {

    // You want Dagger to provide an instance of LoginViewModel from the Login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Gets appComponent from MyApplication available in the base Gradle module
        AppComponent appComponent = ((MyApplication) getApplicationContext()).appComponent;

        // Creates a new instance of LoginComponent
        // Injects the component to populate the @Inject fields
        DaggerLoginComponent.factory().create(appComponent).inject(this);

        // Now you can access loginViewModel
    }
}

LoginViewModel תלוי ב-UserRepository; וכדי ש-LoginComponent יהיו ניתן לגשת אליו דרך AppComponent, AppComponent צריך לחשוף אותו הממשק שלו:

Kotlin

@Singleton
@Component
interface AppComponent {
    fun userRepository(): UserRepository
}

Java

@Singleton
@Component
public interface AppComponent {
    UserRepository userRepository();
}

כללי ההיקף עם רכיבים תלויים פועלים באותו אופן כמו עם של רכיבי המשנה. בגלל ש-LoginComponent משתמש במופע של AppComponent, הם לא יכולים להשתמש באותה הערה של היקף.

אם רוצים להגדיר את ההיקף מ-LoginViewModel עד LoginComponent, צריך לעשות זאת כך: בעבר השתמשת בהערה @ActivityScope המותאמת אישית.

Kotlin

@ActivityScope
@Component(dependencies = [AppComponent::class])
interface LoginComponent { ... }

@ActivityScope
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

Java

@ActivityScope
@Component(dependencies = AppComponent.class)
public interface LoginComponent { ... }

@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

שיטות מומלצות

  • הערך ApplicationComponent תמיד צריך להיות במודול app.

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

  • למודולים של Gradle שמיועדים לכלי שירות או לעוזרים ולא צריכים כדי לבנות תרשים (לכן תצטרכו רכיב של צלבון), צור ותחשוף את המודולים של Dagger באמצעות השיטות @Provides ו- @Binds של המחלקות האלה לא תומכים בהזרקה של constructor.

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