استخدام 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 المتاحة في وحدة Gradle على app.

في ما يلي تعريفات للفئات و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 التي تتضمّن وحدة الدرجة 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 و @الإعمل لهما من هذه الفئات التي لا تتيح حقن دالة الإنشاء.

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