استفاده از Dagger در برنامه های چند ماژول

پروژه ای با چندین ماژول 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 ، تنظیمات مشابهی خواهید داشت.

یک قانون کلی برای یک پروژه چند ماژول این است که ماژول های هم سطح نباید به یکدیگر وابسته باشند. اگر چنین کردند، در نظر بگیرید که آیا منطق مشترک (وابستگی های بین آنها) باید بخشی از ماژول والد باشد یا خیر. اگر چنین است، برای انتقال کلاس ها به ماژول والد، Refactor کنید. اگر نه، یک ماژول جدید ایجاد کنید که ماژول والد را گسترش دهد و هر دو ماژول اصلی ماژول جدید را گسترش دهند.

به عنوان بهترین روش، معمولاً در موارد زیر یک جزء در یک ماژول ایجاد می کنید:

  • مانند LoginActivityComponent باید تزریق فیلد را انجام دهید.

  • شما باید مانند LoginComponent اشیاء را محدوده بندی کنید.

اگر هیچ یک از این موارد صدق نمی کند و باید به Dagger بگویید چگونه اشیاء را از آن ماژول ارائه کند، اگر تزریق ساخت برای آن کلاس ها امکان پذیر نیست، یک ماژول Dagger را با متدهای @Provides یا @Binds ایجاد و در معرض دید قرار دهید.

پیاده سازی با اجزای فرعی Dagger

صفحه سند استفاده از خنجر در برنامه‌های اندروید نحوه ایجاد و استفاده از اجزای فرعی را پوشش می‌دهد. با این حال، نمی توانید از همان کد استفاده کنید زیرا ماژول های ویژگی در مورد ماژول app اطلاعاتی ندارند. به عنوان مثال، اگر به یک جریان ورود معمولی و کدی که در صفحه قبل داریم فکر می کنید، دیگر کامپایل نمی شود:

کاتلین

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

جاوا

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 برای جریان ورود به سیستم ارائه می دهد:

کاتلین

interface LoginComponentProvider {
    fun provideLoginComponent(): LoginComponent
}

جاوا

public interface LoginComponentProvider {
   public LoginComponent provideLoginComponent();
}

اکنون، LoginActivity از آن رابط به جای قطعه کد تعریف شده در بالا استفاده می کند:

کاتلین

class LoginActivity: Activity() {
  ...

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

    loginComponent.inject(this)
    ...
  }
}

جاوا

public class LoginActivity extends Activity {
    ...

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

        loginComponent.inject(this);

        ...
    }
}

اکنون، MyApplication باید آن رابط را پیاده سازی کند و روش های مورد نیاز را پیاده سازی کند:

کاتلین

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

جاوا

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 که بخشی از ماژول app Gradle هستند آورده شده است:

کاتلین

// 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 { ... }

جاوا

// 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 نیاز دارد:

کاتلین

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

جاوا

// 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 ایجاد کنیم که برای تزریق LoginActivity به AppComponent بستگی دارد:

کاتلین

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

جاوا

// 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 باید ارسال شود. برای انجام آن از کارخانه مؤلفه استفاده کنید:

کاتلین

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

جاوا

@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() را فراخوانی کند.

کاتلین

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

جاوا

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 باید آن را در رابط کاربری خود نشان دهد:

کاتلین

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

جاوا

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

قوانین محدوده با اجزای وابسته به همان روشی که با اجزای فرعی کار می کنند. از آنجا که LoginComponent از نمونه ای از AppComponent استفاده می کند، نمی توانند از حاشیه نویسی یکسان استفاده کنند.

اگر می‌خواهید LoginViewModel به LoginComponent تغییر دهید، همانطور که قبلاً با استفاده از حاشیه‌نویسی سفارشی @ActivityScope انجام دادید، این کار را انجام می‌دهید.

کاتلین

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

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

جاوا

@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 دسترسی پیدا کنید.