استخدام Dagerer في تطبيقات متعددة الوحدات

يُعرف المشروع الذي يحتوي على وحدات Gradle المتعددة باسم مشروع متعدد الوحدات. في مشروع متعدد الوحدات يتم شحنه كحزمة APK واحدة بدون أي ميزة من المعتاد أن يكون هناك وحدة app يمكن أن تعتمد على معظم من مشروعك ووحدة base أو core التي التي تعتمد عليها الوحدات عادةً. تحتوي وحدة app عادةً على Application، بينما في base تحتوي على جميع الفئات المشتركة المشتركة بين جميع الوحدات في مشروعك.

تعد الوحدة app مكانًا جيدًا للإعلان عن مكوِّن التطبيق (على سبيل المثال، مثل، ApplicationComponent في الصورة أدناه) يمكنها توفير كائنات التي قد تحتاجها المكونات الأخرى بالإضافة إلى المقاطع المفردة لتطبيقك. على سبيل المثال، مثلاً، فئات مثل OkHttpClient وأدوات تحليل JSON وأدوات وصول قاعدة البيانات أو SharedPreferences التي يمكن تحديدها في الوحدة core، سيتم توفيره من خلال السمة ApplicationComponent المحدّدة في وحدة app.

في وحدة app، قد تكون لديك أيضًا مكونات أخرى ذات عمر أقصر. من الأمثلة على ذلك UserComponent بإعدادات خاصة بالمستخدم. (مثل UserSession) بعد تسجيل الدخول.

في الوحدات المختلفة لمشروعك، يمكنك تحديد نموذج واحد على الأقل مكون فرعي له منطق خاص بهذه الوحدة كما هو موضح في الشكل 1.

الشكل 1. مثال على رسم بياني لـ Dagger في مشروع متعدد الوحدات

على سبيل المثال، في وحدة login، يمكنك وضع LoginComponent. محددة بتعليق توضيحي @ModuleScope مخصص يمكنه توفير كائنات مشتركة إلى هذه الميزة مثل LoginRepository. وداخل هذه الوحدة، يمكنك أيضًا تضم مكونات أخرى تعتمد على LoginComponent ذات نطاق مخصص مختلف النطاق، على سبيل المثال @FeatureScope بالنسبة إلى LoginActivityComponent أو TermsAndConditionsComponent حيث يمكن استخدام منطق أكثر تحديدًا للميزة مثل ViewModel من العناصر.

بالنسبة إلى الوحدات الأخرى مثل Registration، سيكون لديك إعداد مشابه.

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

كأفضل ممارسة، يمكنك بشكل عام إنشاء مكون في في الحالات التالية:

  • يجب إدخال الحقل، كما هو الحال في 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، تحتاج المكونات إلى معرفة مكوناتها الفرعية. هذه المعلومات في وحدة Dagger مضافة إلى المكون الأصلي (مثل SubcomponentsModule في استخدام Dagger في تطبيقات Android).

ولسوء الحظ، مع الاعتماد العكسي بين التطبيق الميزة، فلن يكون المكوِّن الفرعي مرئيًا من وحدة app لأن أنه ليس في مسار الإصدار. على سبيل المثال، تم تحديد 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 { ... }

في وحدة Gradle الخاصة بك في "login" التي تتضمن وحدة Gradle 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)، وإنشاء رسم بياني وحدات Dagger العامة التي تتضمن طرق @Provides و @Binds لتلك الفئات لا تدعم حقن الدالة الإنشائية.

  • لاستخدام Dagger في تطبيق Android يضم وحدات ميزات، عليك استخدام الأخرى أن يتمكنوا من الوصول إلى التبعيات التي يقدمها تم تحديد ApplicationComponent في الوحدة app.