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

صفحه اصول اولیه Dagger توضیح داد که چگونه Dagger می تواند به شما در خودکارسازی تزریق وابستگی در برنامه خود کمک کند. با Dagger، نیازی به نوشتن کدهای خسته کننده و مستعد خطای دیگ بخار ندارید.

خلاصه بهترین شیوه ها

  • از تزریق سازنده با @Inject برای اضافه کردن انواع به نمودار Dagger هر زمان که ممکن است استفاده کنید. وقتی اینطور نیست:
    • از @Binds استفاده کنید تا به Dagger بگویید که یک رابط باید کدام پیاده سازی را داشته باشد.
    • از @Provides استفاده کنید تا به Dagger بگویید چگونه کلاس هایی را ارائه کند که پروژه شما متعلق به آنها نیست.
  • ماژول ها را فقط یک بار در یک جزء باید اعلام کنید.
  • بسته به مدت زمان استفاده از حاشیه نویسی، حاشیه نویسی دامنه را نام ببرید. به عنوان مثال می‌توان به @ApplicationScope ، @LoggedUserScope و @ActivityScope اشاره کرد.

افزودن وابستگی ها

برای استفاده از Dagger در پروژه خود، این وابستگی ها را به برنامه خود در فایل build.gradle خود اضافه کنید. می توانید آخرین نسخه Dagger را در این پروژه GitHub پیدا کنید.

کاتلین

plugins {
  id 'kotlin-kapt'
}

dependencies {
    implementation 'com.google.dagger:dagger:2.x'
    kapt 'com.google.dagger:dagger-compiler:2.x'
}

جاوا

dependencies {
    implementation 'com.google.dagger:dagger:2.x'
    annotationProcessor 'com.google.dagger:dagger-compiler:2.x'
}

خنجر در اندروید

یک نمونه برنامه اندروید را با نمودار وابستگی از شکل 1 در نظر بگیرید.

LoginActivity به LoginViewModel بستگی دارد که به UserRepository بستگی دارد که به UserLocalDataSource و UserRemoteDataSource بستگی دارد که به نوبه خود به Retrofit بستگی دارد.

شکل 1. نمودار وابستگی کد مثال

در اندروید، معمولاً یک نمودار Dagger ایجاد می‌کنید که در کلاس برنامه شما زندگی می‌کند، زیرا می‌خواهید یک نمونه از نمودار تا زمانی که برنامه در حال اجرا است در حافظه باشد. به این ترتیب نمودار به چرخه عمر اپلیکیشن متصل می شود. در برخی موارد، ممکن است بخواهید زمینه برنامه را در نمودار نیز در دسترس داشته باشید. برای آن، شما همچنین باید گراف را در کلاس Application قرار دهید. یکی از مزایای این رویکرد این است که نمودار برای سایر کلاس های فریم ورک اندروید در دسترس است. علاوه بر این، با استفاده از یک کلاس Application سفارشی در تست ها، تست را ساده می کند.

از آنجا که رابطی که نمودار را ایجاد می کند با @Component حاشیه نویسی شده است، می توانید آن را ApplicationComponent یا ApplicationGraph بنامید. همانطور که در قطعه کد زیر نشان داده شده است، معمولاً نمونه ای از آن مؤلفه را در کلاس Application سفارشی خود نگه می دارید و هر بار که به نمودار برنامه نیاز داشتید، آن را فراخوانی می کنید:

کاتلین

// Definition of the Application graph
@Component
interface ApplicationComponent { ... }

// appComponent lives in the Application class to share its lifecycle
class MyApplication: Application() {
    // Reference to the application graph that is used across the whole app
    val appComponent = DaggerApplicationComponent.create()
}

جاوا

// Definition of the Application graph
@Component
public interface ApplicationComponent {
}

// appComponent lives in the Application class to share its lifecycle
public class MyApplication extends Application {

    // Reference to the application graph that is used across the whole app
    ApplicationComponent appComponent = DaggerApplicationComponent.create();
}

از آنجایی که برخی کلاس‌های فریمورک اندروید مانند فعالیت‌ها و قطعات توسط سیستم نمونه‌سازی می‌شوند، Dagger نمی‌تواند آنها را برای شما ایجاد کند. برای فعالیت ها به طور خاص، هر کد اولیه باید وارد متد onCreate() شود. این بدان معناست که شما نمی توانید مانند نمونه های قبلی از حاشیه نویسی @Inject در سازنده کلاس (تزریق سازنده) استفاده کنید. در عوض، باید از تزریق میدانی استفاده کنید.

به جای ایجاد وابستگی های مورد نیاز یک فعالیت در متد onCreate() ، می خواهید Dagger آن وابستگی ها را برای شما پر کند. برای تزریق فیلد، به جای آن، حاشیه نویسی @Inject را در فیلدهایی که می خواهید از نمودار Dagger دریافت کنید، اعمال کنید.

کاتلین

class LoginActivity: Activity() {
    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject lateinit var loginViewModel: LoginViewModel
}

جاوا

public class LoginActivity extends Activity {

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

برای سادگی، LoginViewModel یک ViewModel اجزای معماری اندروید نیست. این فقط یک کلاس معمولی است که به عنوان ViewModel عمل می کند. برای اطلاعات بیشتر درباره نحوه تزریق این کلاس‌ها، کد موجود در اجرای رسمی Android Blueprints Dagger را در شاخه dev-dagger بررسی کنید.

یکی از ملاحظات Dagger این است که فیلدهای تزریق شده نمی توانند خصوصی باشند. آنها باید حداقل قابلیت مشاهده بسته خصوصی مانند کد قبلی را داشته باشند.

فعالیت های تزریقی

Dagger باید بداند که LoginActivity باید به نمودار دسترسی داشته باشد تا ViewModel مورد نیاز خود را ارائه دهد. در صفحه مبانی Dagger ، از واسط @Component برای دریافت اشیاء از نمودار با نمایش توابعی با نوع بازگشتی آنچه می‌خواهید از نمودار دریافت کنید، استفاده کردید. در این مورد، شما باید به Dagger در مورد یک شی ( LoginActivity در این مورد) که نیاز به یک وابستگی برای تزریق دارد، بگویید. برای آن، تابعی را در معرض دید قرار می دهید که شی درخواست تزریق را به عنوان پارامتر می گیرد.

کاتلین

@Component
interface ApplicationComponent {
    // This tells Dagger that LoginActivity requests injection so the graph needs to
    // satisfy all the dependencies of the fields that LoginActivity is requesting.
    fun inject(activity: LoginActivity)
}

جاوا

@Component
public interface ApplicationComponent {
    // This tells Dagger that LoginActivity requests injection so the graph needs to
    // satisfy all the dependencies of the fields that LoginActivity is injecting.
    void inject(LoginActivity loginActivity);
}

این تابع به Dagger می گوید که LoginActivity می خواهد به نمودار دسترسی داشته باشد و درخواست تزریق می کند. Dagger باید تمام وابستگی‌هایی را که LoginActivity به آن نیاز دارد برآورده کند ( LoginViewModel با وابستگی‌های خودش). اگر چندین کلاس دارید که درخواست تزریق دارند، باید به طور خاص همه آنها را با نوع دقیق آنها در کامپوننت اعلام کنید. برای مثال، اگر LoginActivity و RegistrationActivity درخواست تزریق داشتند، به جای یک روش عمومی که هر دو مورد را پوشش می‌دهد، دو متد inject() خواهید داشت. یک متد inject() عمومی به Dagger نمی گوید که چه چیزی باید ارائه شود. توابع موجود در اینترفیس می توانند هر نامی داشته باشند، اما فراخوانی آنها inject() هنگامی که آنها شیء را برای تزریق به عنوان پارامتر دریافت می کنند، یک قرارداد در Dagger است.

برای تزریق یک شی در اکتیویتی، می‌توانید از appComponent تعریف‌شده در کلاس Application خود استفاده کنید و متد inject() را فراخوانی کنید و در نمونه‌ای از اکتیویتی که درخواست تزریق می‌کند ارسال کنید.

هنگام استفاده از اکتیویتی ها، قبل از فراخوانی () super.onCreate() ، Dagger را در متد onCreate() ) اکتیویتی تزریق کنید تا از مشکلات مربوط به بازیابی قطعه جلوگیری کنید. در طول مرحله بازیابی در super.onCreate() یک اکتیویتی قطعاتی را که ممکن است بخواهند به اتصالات اکتیویتی دسترسی داشته باشند متصل می کند.

هنگام استفاده از قطعات، Dagger را در متد onAttach() قطعه تزریق کنید. در این مورد، این کار را می توان قبل یا بعد از فراخوانی super.onAttach() انجام داد.

کاتلین

class LoginActivity: Activity() {
    // You want Dagger to provide an instance of LoginViewModel from the graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        // Make Dagger instantiate @Inject fields in LoginActivity
        (applicationContext as MyApplication).appComponent.inject(this)
        // Now loginViewModel is available

        super.onCreate(savedInstanceState)
    }
}

// @Inject tells Dagger how to create instances of LoginViewModel
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

جاوا

public class LoginActivity extends Activity {

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

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // Make Dagger instantiate @Inject fields in LoginActivity
        ((MyApplication) getApplicationContext()).appComponent.inject(this);
        // Now loginViewModel is available

        super.onCreate(savedInstanceState);
    }
}

public class LoginViewModel {

    private final UserRepository userRepository;

    // @Inject tells Dagger how to create instances of LoginViewModel
    @Inject
    public LoginViewModel(UserRepository userRepository) {
        this.userRepository = userRepository;
    }
}

بیایید به Dagger بگوییم که چگونه بقیه وابستگی‌ها را برای ساخت نمودار فراهم کند:

کاتلین

class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

class UserLocalDataSource @Inject constructor() { ... }
class UserRemoteDataSource @Inject constructor(
    private val loginService: LoginRetrofitService
) { ... }

جاوا

public class UserRepository {

    private final UserLocalDataSource userLocalDataSource;
    private final UserRemoteDataSource userRemoteDataSource;

    @Inject
    public UserRepository(UserLocalDataSource userLocalDataSource, UserRemoteDataSource userRemoteDataSource) {
        this.userLocalDataSource = userLocalDataSource;
        this.userRemoteDataSource = userRemoteDataSource;
    }
}

public class UserLocalDataSource {

    @Inject
    public UserLocalDataSource() {}
}

public class UserRemoteDataSource {

    private final LoginRetrofitService loginRetrofitService;

    @Inject
    public UserRemoteDataSource(LoginRetrofitService loginRetrofitService) {
        this.loginRetrofitService = loginRetrofitService;
    }
}

ماژول های خنجر

برای این مثال، شما از کتابخانه شبکه Retrofit استفاده می کنید. UserRemoteDataSource به LoginRetrofitService وابستگی دارد. با این حال، روش ایجاد یک نمونه از LoginRetrofitService با آنچه تاکنون انجام داده اید متفاوت است. این یک نمونه کلاسی نیست. این نتیجه فراخوانی Retrofit.Builder() و ارسال پارامترهای مختلف برای پیکربندی سرویس ورود است.

به غیر از حاشیه نویسی @Inject ، راه دیگری برای گفتن به Dagger وجود دارد که چگونه یک نمونه از یک کلاس را ارائه دهد: اطلاعات داخل ماژول های Dagger. ماژول Dagger کلاسی است که با @Module حاشیه نویسی شده است. در آنجا می‌توانید وابستگی‌ها را با حاشیه‌نویسی @Provides تعریف کنید.

کاتلین

// @Module informs Dagger that this class is a Dagger Module
@Module
class NetworkModule {

    // @Provides tell Dagger how to create instances of the type that this function
    // returns (i.e. LoginRetrofitService).
    // Function parameters are the dependencies of this type.
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService {
        // Whenever Dagger needs to provide an instance of type LoginRetrofitService,
        // this code (the one inside the @Provides method) is run.
        return Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService::class.java)
    }
}

جاوا

// @Module informs Dagger that this class is a Dagger Module
@Module
public class NetworkModule {

    // @Provides tell Dagger how to create instances of the type that this function
    // returns (i.e. LoginRetrofitService).
    // Function parameters are the dependencies of this type.
    @Provides
    public LoginRetrofitService provideLoginRetrofitService() {
        // Whenever Dagger needs to provide an instance of type LoginRetrofitService,
        // this code (the one inside the @Provides method) is run.
        return new Retrofit.Builder()
                .baseUrl("https://example.com")
                .build()
                .create(LoginService.class);
    }
}

ماژول ها راهی برای جمع بندی معنایی اطلاعات در مورد نحوه ارائه اشیا هستند. همانطور که می بینید، شما کلاس NetworkModule را برای گروه بندی منطق ارائه اشیاء مرتبط با شبکه فراخوانی کردید. اگر برنامه گسترش یابد، می‌توانید نحوه ارائه OkHttpClient یا نحوه پیکربندی Gson یا Moshi را نیز در اینجا اضافه کنید.

وابستگی های یک متد @Provides پارامترهای آن متد هستند. برای روش قبلی، LoginRetrofitService را می توان بدون وابستگی ارائه کرد زیرا این روش هیچ پارامتری ندارد. اگر یک OkHttpClient به عنوان پارامتر اعلام کرده بودید، Dagger باید یک نمونه OkHttpClient را از نمودار ارائه دهد تا وابستگی های LoginRetrofitService را برآورده کند. به عنوان مثال:

کاتلین

@Module
class NetworkModule {
    // Hypothetical dependency on LoginRetrofitService
    @Provides
    fun provideLoginRetrofitService(
        okHttpClient: OkHttpClient
    ): LoginRetrofitService { ... }
}

جاوا

@Module
public class NetworkModule {

    @Provides
    public LoginRetrofitService provideLoginRetrofitService(OkHttpClient okHttpClient) {
        ...
    }
}

برای اینکه نمودار Dagger از این ماژول مطلع شود، باید آن را به صورت زیر به واسط @Component اضافه کنید:

کاتلین

// The "modules" attribute in the @Component annotation tells Dagger what Modules
// to include when building the graph
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
    ...
}

جاوا

// The "modules" attribute in the @Component annotation tells Dagger what Modules
// to include when building the graph
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
    ...
}

روش پیشنهادی برای افزودن انواع به نمودار Dagger با استفاده از تزریق سازنده (یعنی با حاشیه نویسی @Inject در سازنده کلاس) است. گاهی اوقات، این امکان پذیر نیست و باید از ماژول های Dagger استفاده کنید. یک مثال زمانی است که می خواهید Dagger از نتیجه یک محاسبات برای تعیین نحوه ایجاد یک نمونه از یک شی استفاده کند. هر زمان که باید نمونه ای از آن نوع ارائه کند، Dagger کد را در متد @Provides اجرا می کند.

نمودار Dagger در مثال در حال حاضر به این صورت است:

نمودار نمودار وابستگی LoginActivity

شکل 2. نمایش گراف با LoginActivity در حال تزریق توسط Dagger

نقطه ورود به نمودار LoginActivity است. از آنجایی که LoginActivity LoginViewModel تزریق می کند، Dagger نموداری می سازد که می داند چگونه نمونه ای از LoginViewModel و به صورت بازگشتی از وابستگی های آن ارائه کند. Dagger به دلیل حاشیه نویسی @Inject در سازنده کلاس ها می داند چگونه این کار را انجام دهد.

در داخل ApplicationComponent تولید شده توسط Dagger، یک روش کارخانه ای برای دریافت نمونه هایی از تمام کلاس هایی که می داند چگونه ارائه کند وجود دارد. در این مثال، Dagger به NetworkModule موجود در ApplicationComponent تفویض می‌کند تا نمونه‌ای از LoginRetrofitService را دریافت کند.

دامنه های خنجر

دامنه ها در صفحه مبانی Dagger به عنوان راهی برای داشتن یک نمونه منحصر به فرد از یک نوع در یک جزء ذکر شده است. این همان چیزی است که از محدوده بندی یک نوع در چرخه عمر قطعه استفاده می شود.

از آنجا که ممکن است بخواهید از UserRepository در سایر ویژگی‌های برنامه استفاده کنید و ممکن است نخواهید هر بار که به آن نیاز دارید یک شی جدید ایجاد کنید، می‌توانید آن را به عنوان یک نمونه منحصر به فرد برای کل برنامه تعیین کنید. برای LoginRetrofitService هم همین‌طور است: ایجاد آن می‌تواند گران باشد، و همچنین می‌خواهید یک نمونه منحصربه‌فرد از آن شی مورد استفاده مجدد قرار گیرد. ایجاد یک نمونه از UserRemoteDataSource آنقدر گران نیست، بنابراین محدود کردن آن به چرخه عمر مؤلفه ضروری نیست.

@Singleton تنها حاشیه نویسی دامنه است که با بسته javax.inject ارائه می شود. می توانید از آن برای حاشیه نویسی ApplicationComponent و اشیایی که می خواهید در کل برنامه استفاده مجدد کنید استفاده کنید.

کاتلین

@Singleton
@Component(modules = [NetworkModule::class])
interface ApplicationComponent {
    fun inject(activity: LoginActivity)
}

@Singleton
class UserRepository @Inject constructor(
    private val localDataSource: UserLocalDataSource,
    private val remoteDataSource: UserRemoteDataSource
) { ... }

@Module
class NetworkModule {
    // Way to scope types inside a Dagger Module
    @Singleton
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService { ... }
}

جاوا

@Singleton
@Component(modules = NetworkModule.class)
public interface ApplicationComponent {
    void inject(LoginActivity loginActivity);
}

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

@Module
public class NetworkModule {

    @Singleton
    @Provides
    public LoginRetrofitService provideLoginRetrofitService() { ... }
}

مراقب باشید هنگام اعمال scope روی اشیا، نشت حافظه ایجاد نشود. تا زمانی که مولفه scoped در حافظه است، شی ایجاد شده نیز در حافظه است. از آنجایی که ApplicationComponent هنگام راه‌اندازی برنامه (در کلاس Application ) ایجاد می‌شود، زمانی که برنامه از بین می‌رود، از بین می‌رود. بنابراین، نمونه منحصر به فرد UserRepository همیشه در حافظه باقی می ماند تا زمانی که برنامه از بین برود.

اجزای فرعی خنجر

اگر جریان ورود به سیستم شما (که توسط یک LoginActivity مدیریت می شود) از چند قطعه تشکیل شده است، باید از همان نمونه LoginViewModel در همه قطعات استفاده مجدد کنید. @Singleton نمی تواند LoginViewModel برای استفاده مجدد از نمونه به دلایل زیر حاشیه نویسی کند:

  1. نمونه LoginViewModel پس از پایان جریان در حافظه باقی می ماند.

  2. شما یک نمونه متفاوت از LoginViewModel برای هر جریان ورود می خواهید. به عنوان مثال، اگر کاربر از سیستم خارج شود، شما یک نمونه متفاوت از LoginViewModel را می خواهید، به جای نمونه مشابه زمانی که کاربر برای اولین بار وارد سیستم شده است.

برای گسترش LoginViewModel به چرخه حیات LoginActivity ، باید یک جزء جدید (یک زیرگراف جدید) برای جریان ورود و یک محدوده جدید ایجاد کنید.

بیایید یک نمودار خاص برای جریان ورود ایجاد کنیم.

کاتلین

@Component
interface LoginComponent {}

جاوا

@Component
public interface LoginComponent {
}

اکنون، LoginActivity باید از LoginComponent تزریق شود زیرا دارای یک پیکربندی خاص برای ورود است. این کار مسئولیت تزریق LoginActivity از کلاس ApplicationComponent حذف می کند.

کاتلین

@Component
interface LoginComponent {
    fun inject(activity: LoginActivity)
}

جاوا

@Component
public interface LoginComponent {
    void inject(LoginActivity loginActivity);
}

LoginComponent باید بتواند به اشیاء از ApplicationComponent دسترسی داشته باشد زیرا LoginViewModel به UserRepository بستگی دارد. روشی که می‌توان به Dagger گفت که می‌خواهید یک مؤلفه جدید از بخشی از مؤلفه دیگر استفاده کند، با مؤلفه‌های فرعی Dagger است. مؤلفه جدید باید جزء فرعی مؤلفه حاوی منابع مشترک باشد.

مولفه های فرعی اجزایی هستند که گراف شی یک جزء اصلی را به ارث می برند و گسترش می دهند. بنابراین، تمام اشیاء ارائه شده در مؤلفه والد در زیر مؤلفه نیز ارائه می شوند. به این ترتیب، یک شی از یک جزء فرعی می تواند به یک شی ارائه شده توسط مولفه والد وابسته باشد.

برای ایجاد نمونه هایی از مولفه های فرعی، به یک نمونه از مولفه والد نیاز دارید. بنابراین، اشیاء ارائه شده توسط مؤلفه والد به مؤلفه فرعی همچنان در محدوده مؤلفه والد قرار دارند.

در مثال، باید LoginComponent به عنوان زیرمجموعه ApplicationComponent تعریف کنید. برای انجام این کار، LoginComponent با @Subcomponent حاشیه نویسی کنید:

کاتلین

// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent
@Subcomponent
interface LoginComponent {

    // This tells Dagger that LoginActivity requests injection from LoginComponent
    // so that this subcomponent graph needs to satisfy all the dependencies of the
    // fields that LoginActivity is injecting
    fun inject(loginActivity: LoginActivity)
}

جاوا

// @Subcomponent annotation informs Dagger this interface is a Dagger Subcomponent
@Subcomponent
public interface LoginComponent {

    // This tells Dagger that LoginActivity requests injection from LoginComponent
    // so that this subcomponent graph needs to satisfy all the dependencies of the
    // fields that LoginActivity is injecting
    void inject(LoginActivity loginActivity);
}

همچنین باید یک کارخانه فرعی در داخل LoginComponent تعریف کنید تا ApplicationComponent بداند که چگونه نمونه هایی از LoginComponent ایجاد کند.

کاتلین

@Subcomponent
interface LoginComponent {

    // Factory that is used to create instances of this subcomponent
    @Subcomponent.Factory
    interface Factory {
        fun create(): LoginComponent
    }

    fun inject(loginActivity: LoginActivity)
}

جاوا

@Subcomponent
public interface LoginComponent {

    // Factory that is used to create instances of this subcomponent
    @Subcomponent.Factory
    interface Factory {
        LoginComponent create();
    }

    void inject(LoginActivity loginActivity);
}

برای اینکه به Dagger بگویید که LoginComponent یک جزء فرعی از ApplicationComponent است، باید آن را با موارد زیر مشخص کنید:

  1. ایجاد یک ماژول Dagger جدید (به عنوان مثال SubcomponentsModule ) که کلاس زیرمجموعه را به ویژگی subcomponents حاشیه نویسی منتقل می کند.

    کاتلین

    // The "subcomponents" attribute in the @Module annotation tells Dagger what
    // Subcomponents are children of the Component this module is included in.
    @Module(subcomponents = LoginComponent::class)
    class SubcomponentsModule {}
    

    جاوا

    // The "subcomponents" attribute in the @Module annotation tells Dagger what
    // Subcomponents are children of the Component this module is included in.
    @Module(subcomponents = LoginComponent.class)
    public class SubcomponentsModule {
    }
    
  2. اضافه کردن ماژول جدید (یعنی SubcomponentsModule ) به ApplicationComponent :

    کاتلین

    // Including SubcomponentsModule, tell ApplicationComponent that
    // LoginComponent is its subcomponent.
    @Singleton
    @Component(modules = [NetworkModule::class, SubcomponentsModule::class])
    interface ApplicationComponent {
    }
    

    جاوا

    // Including SubcomponentsModule, tell ApplicationComponent that
    // LoginComponent is its subcomponent.
    @Singleton
    @Component(modules = {NetworkModule.class, SubcomponentsModule.class})
    public interface ApplicationComponent {
    }
    

    توجه داشته باشید که ApplicationComponent دیگر نیازی به تزریق LoginActivity ندارد زیرا این مسئولیت اکنون به LoginComponent تعلق دارد، بنابراین می توانید متد inject() از ApplicationComponent حذف کنید.

    مصرف کنندگان ApplicationComponent باید بدانند که چگونه نمونه هایی از LoginComponent ایجاد کنند. مؤلفه والد باید روشی را در رابط خود اضافه کند تا به مصرف کنندگان اجازه دهد نمونه هایی از مولفه فرعی را از نمونه ای از مؤلفه والد ایجاد کنند:

  3. کارخانه ای که نمونه هایی از LoginComponent را در رابط ایجاد می کند را در معرض نمایش قرار دهید:

    کاتلین

    @Singleton
    @Component(modules = [NetworkModule::class, SubcomponentsModule::class])
    interface ApplicationComponent {
    // This function exposes the LoginComponent Factory out of the graph so consumers
    // can use it to obtain new instances of LoginComponent
    fun loginComponent(): LoginComponent.Factory
    }
    

    جاوا

    @Singleton
    @Component(modules = { NetworkModule.class, SubcomponentsModule.class} )
    public interface ApplicationComponent {
    // This function exposes the LoginComponent Factory out of the graph so consumers
    // can use it to obtain new instances of LoginComponent
    LoginComponent.Factory loginComponent();
    }
    

اختصاص دامنه به اجزای فرعی

اگر پروژه را می سازید، می توانید نمونه هایی از ApplicationComponent و LoginComponent ایجاد کنید. ApplicationComponent به چرخه عمر برنامه متصل است زیرا می خواهید تا زمانی که برنامه در حافظه است از همان نمونه نمودار استفاده کنید.

چرخه عمر LoginComponent چیست؟ یکی از دلایلی که شما به LoginComponent نیاز دارید این است که شما باید همان نمونه LoginViewModel را بین قطعات مرتبط با Login به اشتراک بگذارید. اما همچنین، هر زمان که یک جریان ورود جدید وجود داشته باشد، شما نمونه های مختلفی از LoginViewModel را می خواهید. LoginActivity طول عمر مناسب برای LoginComponent است: برای هر فعالیت جدید، شما به یک نمونه جدید از LoginComponent و قطعاتی نیاز دارید که بتواند از آن نمونه LoginComponent استفاده کند.

از آنجایی که LoginComponent به چرخه حیات LoginActivity متصل است، باید به همان روشی که ارجاع به applicationComponent را در کلاس Application نگه داشتید، یک مرجع به کامپوننت در اکتیویتی نگه دارید. به این ترتیب، قطعات می توانند به آن دسترسی داشته باشند.

کاتلین

class LoginActivity: Activity() {
    // Reference to the Login graph
    lateinit var loginComponent: LoginComponent
    ...
}

جاوا

public class LoginActivity extends Activity {

    // Reference to the Login graph
    LoginComponent loginComponent;

    ...
}

توجه داشته باشید که متغیر loginComponent با @Inject حاشیه نویسی نشده است زیرا انتظار ندارید آن متغیر توسط Dagger ارائه شود.

می‌توانید از ApplicationComponent برای دریافت ارجاع به LoginComponent و سپس تزریق LoginActivity به صورت زیر استفاده کنید:

کاتلین

class LoginActivity: Activity() {
    // Reference to the Login graph
    lateinit var loginComponent: LoginComponent

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    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)

        // Now loginViewModel is available

        super.onCreate(savedInstanceState)
    }
}

جاوا

public class LoginActivity extends Activity {

    // Reference to the Login graph
    LoginComponent loginComponent;

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

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

        // Now loginViewModel is available

        super.onCreate(savedInstanceState);
    }
}

LoginComponent در متد onCreate() اکتیویتی ایجاد می شود و زمانی که اکتیویتی از بین می رود بطور ضمنی از بین می رود.

LoginComponent باید همیشه هر بار که درخواست می شود همان نمونه LoginViewModel را ارائه دهد. شما می توانید با ایجاد یک محدوده حاشیه نویسی سفارشی و حاشیه نویسی هر دو LoginComponent و LoginViewModel با آن اطمینان حاصل کنید. توجه داشته باشید که نمی توانید از حاشیه نویسی @Singleton استفاده کنید زیرا قبلاً توسط مؤلفه والد استفاده شده است و این موضوع را به یک برنامه کاربردی تبدیل می کند (نمونه ای منحصر به فرد برای کل برنامه). شما باید یک محدوده حاشیه نویسی متفاوت ایجاد کنید.

در این مورد، می‌توانستید این محدوده را @LoginScope نامیده باشید، اما این عمل خوبی نیست. نام حاشیه نویسی محدوده نباید واضح باشد به هدفی که انجام می دهد. در عوض، باید بسته به طول عمر آن نامگذاری شود زیرا حاشیه نویسی می تواند توسط مؤلفه های خواهر و برادر مانند RegistrationComponent و SettingsComponent دوباره استفاده شود. به همین دلیل است که باید آن را به جای @LoginScope @ActivityScope بنامید.

کاتلین

// Definition of a custom scope called ActivityScope
@Scope
@Retention(value = AnnotationRetention.RUNTIME)
annotation class ActivityScope

// Classes annotated with @ActivityScope are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@ActivityScope
@Subcomponent
interface LoginComponent { ... }

// A unique instance of LoginViewModel is provided in Components
// annotated with @ActivityScope
@ActivityScope
class LoginViewModel @Inject constructor(
    private val userRepository: UserRepository
) { ... }

جاوا

// Definition of a custom scope called ActivityScope
@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface ActivityScope {}

// Classes annotated with @ActivityScope are scoped to the graph and the same
// instance of that type is provided every time the type is requested.
@ActivityScope
@Subcomponent
public interface LoginComponent { ... }

// A unique instance of LoginViewModel is provided in Components
// annotated with @ActivityScope
@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

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

حال، اگر دو قطعه داشتید که نیاز به LoginViewModel دارند، هر دوی آنها با یک نمونه ارائه می‌شوند. برای مثال، اگر یک LoginUsernameFragment و یک LoginPasswordFragment دارید، باید توسط LoginComponent تزریق شوند:

کاتلین

@ActivityScope
@Subcomponent
interface LoginComponent {

    @Subcomponent.Factory
    interface Factory {
        fun create(): LoginComponent
    }

    // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment
    // request injection from LoginComponent. The graph needs to satisfy
    // all the dependencies of the fields those classes are injecting
    fun inject(loginActivity: LoginActivity)
    fun inject(usernameFragment: LoginUsernameFragment)
    fun inject(passwordFragment: LoginPasswordFragment)
}

جاوا

@ActivityScope
@Subcomponent
public interface LoginComponent {

    @Subcomponent.Factory
    interface Factory {
        LoginComponent create();
    }

    // All LoginActivity, LoginUsernameFragment and LoginPasswordFragment
    // request injection from LoginComponent. The graph needs to satisfy
    // all the dependencies of the fields those classes are injecting
    void inject(LoginActivity loginActivity);
    void inject(LoginUsernameFragment loginUsernameFragment);
    void inject(LoginPasswordFragment loginPasswordFragment);
}

کامپوننت ها به نمونه کامپوننتی که در شی LoginActivity زندگی می کند دسترسی پیدا می کنند. کد مثال برای LoginUserNameFragment در قطعه کد زیر ظاهر می شود:

کاتلین

class LoginUsernameFragment: Fragment() {

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)

        // Obtaining the login graph from LoginActivity and instantiate
        // the @Inject fields with objects from the graph
        (activity as LoginActivity).loginComponent.inject(this)

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

جاوا

public class LoginUsernameFragment extends Fragment {

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        // Obtaining the login graph from LoginActivity and instantiate
        // the @Inject fields with objects from the graph
        ((LoginActivity) getActivity()).loginComponent.inject(this);

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

و همینطور برای LoginPasswordFragment :

کاتلین

class LoginPasswordFragment: Fragment() {

    // Fields that need to be injected by the login graph
    @Inject lateinit var loginViewModel: LoginViewModel

    override fun onAttach(context: Context) {
        super.onAttach(context)

        (activity as LoginActivity).loginComponent.inject(this)

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

جاوا

public class LoginPasswordFragment extends Fragment {

    // Fields that need to be injected by the login graph
    @Inject
    LoginViewModel loginViewModel;

    @Override
    public void onAttach(Context context) {
        super.onAttach(context);

        ((LoginActivity) getActivity()).loginComponent.inject(this);

        // Now you can access loginViewModel here and onCreateView too
        // (shared instance with the Activity and the other Fragment)
    }
}

شکل 3 نشان می دهد که چگونه نمودار Dagger با زیر جزء جدید به نظر می رسد. کلاس‌های دارای نقطه سفید ( UserRepository ، LoginRetrofitService و LoginViewModel ) کلاس‌هایی هستند که یک نمونه منحصربه‌فرد برای اجزای مربوطه خود دارند.

نمودار برنامه پس از افزودن آخرین زیرمجموعه

شکل 3. نمایش نموداری که برای مثال برنامه اندروید ساخته اید

بیایید بخش های نمودار را تجزیه کنیم:

  1. NetworkModule (و بنابراین LoginRetrofitService ) در ApplicationComponent گنجانده شده است زیرا شما آن را در کامپوننت مشخص کرده اید.

  2. UserRepository در ApplicationComponent باقی می ماند زیرا محدوده آن به ApplicationComponent است. اگر پروژه رشد کند، می‌خواهید نمونه مشابهی را در ویژگی‌های مختلف به اشتراک بگذارید (مثلاً ثبت نام).

    از آنجایی که UserRepository بخشی از ApplicationComponent است، وابستگی‌های آن (یعنی UserLocalDataSource و UserRemoteDataSource ) نیز باید در این مؤلفه باشد تا بتواند نمونه‌هایی از UserRepository را ارائه دهد.

  3. LoginViewModel در LoginComponent گنجانده شده است زیرا فقط توسط کلاس های تزریق شده توسط LoginComponent مورد نیاز است. LoginViewModel در ApplicationComponent گنجانده نشده است زیرا هیچ وابستگی در ApplicationComponent به LoginViewModel نیاز ندارد.

    به طور مشابه، اگر محدوده UserRepository به ApplicationComponent وارد نکرده بودید، Dagger به طور خودکار UserRepository و وابستگی های آن را به عنوان بخشی از LoginComponent درج می کرد زیرا در حال حاضر تنها جایی است که UserRepository استفاده می شود.

جدای از محدوده بندی اشیاء در چرخه حیات متفاوت، ایجاد مولفه های فرعی روش خوبی برای کپسوله کردن بخش های مختلف برنامه شما از یکدیگر است .

ساختار برنامه‌تان برای ایجاد زیرگراف‌های مختلف Dagger بسته به جریان برنامه، به برنامه کاربردی‌تر و مقیاس‌پذیرتر از نظر حافظه و زمان راه‌اندازی کمک می‌کند.

بهترین روش ها هنگام ساخت نمودار Dagger

هنگام ساخت نمودار Dagger برای برنامه شما:

  • هنگامی که یک کامپوننت را ایجاد می کنید، باید در نظر بگیرید که چه عنصری مسئول طول عمر آن جزء است. در این حالت، کلاس Application مسئول ApplicationComponent و LoginActivity مسئول LoginComponent است.

  • از محدوده زمانی استفاده کنید که منطقی باشد. استفاده بیش از حد از محدوده می تواند تأثیر منفی بر عملکرد زمان اجرا برنامه شما داشته باشد: تا زمانی که کامپوننت در حافظه باشد، شی در حافظه است و دریافت یک شی با دامنه گران تر است. وقتی Dagger شی را ارائه می دهد، به جای ارائه دهنده نوع کارخانه از قفل DoubleCheck استفاده می کند.

آزمایش پروژه ای که از Dagger استفاده می کند

یکی از مزایای استفاده از چارچوب‌های تزریق وابستگی مانند Dagger این است که تست کد شما را آسان‌تر می‌کند.

تست های واحد

لازم نیست از Dagger برای تست های واحد استفاده کنید. هنگام آزمایش کلاسی که از تزریق سازنده استفاده می کند، نیازی به استفاده از Dagger برای نمونه سازی آن کلاس ندارید. می‌توانید مستقیماً سازنده آن را که در وابستگی‌های جعلی یا ساختگی می‌گذرد، مستقیماً همانطور که اگر حاشیه‌نویسی نمی‌شد تماس بگیرید.

به عنوان مثال، هنگام آزمایش LoginViewModel :

کاتلین

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

class LoginViewModelTest {

    @Test
    fun `Happy path`() {
        // You don't need Dagger to create an instance of LoginViewModel
        // You can pass a fake or mock UserRepository
        val viewModel = LoginViewModel(fakeUserRepository)
        assertEquals(...)
    }
}

جاوا

@ActivityScope
public class LoginViewModel {

    private final UserRepository userRepository;

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

public class LoginViewModelTest {

    @Test
    public void happyPath() {
        // You don't need Dagger to create an instance of LoginViewModel
        // You can pass a fake or mock UserRepository
        LoginViewModel viewModel = new LoginViewModel(fakeUserRepository);
        assertEquals(...);
    }
}

تست های پایان به انتها

برای تست های یکپارچه سازی ، یک تمرین خوب این است که یک TestApplicationComponent برای آزمایش ایجاد کنید. تولید و آزمایش از پیکربندی اجزای متفاوتی استفاده می کند .

این نیاز به طراحی قبلی بیشتر ماژول ها در برنامه شما دارد. مؤلفه آزمایشی مؤلفه تولید را گسترش می دهد و مجموعه متفاوتی از ماژول ها را نصب می کند.

کاتلین

// TestApplicationComponent extends from ApplicationComponent to have them both
// with the same interface methods. You need to include the modules of the
// component here as well, and you can replace the ones you want to override.
// This sample uses FakeNetworkModule instead of NetworkModule
@Singleton
@Component(modules = [FakeNetworkModule::class, SubcomponentsModule::class])
interface TestApplicationComponent : ApplicationComponent {
}

جاوا

// TestApplicationComponent extends from ApplicationComponent to have them both
// with the same interface methods. You need to include the modules of the
// Component here as well, and you can replace the ones you want to override.
// This sample uses FakeNetworkModule instead of NetworkModule
@Singleton
@Component(modules = {FakeNetworkModule.class, SubcomponentsModule.class})
public interface TestApplicationComponent extends ApplicationComponent {
}

FakeNetworkModule یک پیاده سازی جعلی از NetworkModule اصلی دارد. در آنجا می توانید نمونه های جعلی یا مسخره هر چیزی را که می خواهید جایگزین کنید ارائه دهید.

کاتلین

// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService
// that you can use in your tests.
@Module
class FakeNetworkModule {
    @Provides
    fun provideLoginRetrofitService(): LoginRetrofitService {
        return FakeLoginService()
    }
}

جاوا

// In the FakeNetworkModule, pass a fake implementation of LoginRetrofitService
// that you can use in your tests.
@Module
public class FakeNetworkModule {

    @Provides
    public LoginRetrofitService provideLoginRetrofitService() {
        return new FakeLoginService();
    }
}

در آزمایش‌های ادغام یا پایان به انتها، از TestApplication استفاده می‌کنید که TestApplicationComponent به جای ApplicationComponent ایجاد می‌کند.

کاتلین

// Your test application needs an instance of the test graph
class MyTestApplication: MyApplication() {
    override val appComponent = DaggerTestApplicationComponent.create()
}

جاوا

// Your test application needs an instance of the test graph
public class MyTestApplication extends MyApplication {
    ApplicationComponent appComponent = DaggerTestApplicationComponent.create();
}

سپس، این برنامه آزمایشی در یک TestRunner سفارشی استفاده می شود که از آن برای اجرای تست های ابزار دقیق استفاده می کنید. برای اطلاعات بیشتر در این مورد، استفاده از خنجر را در نرم افزار کد برنامه Android خود بررسی کنید.

کار با ماژول های Dagger

ماژول های خنجر راهی برای کپسوله کردن نحوه ارائه اشیاء به روش معنایی هستند. شما می توانید ماژول ها را در کامپوننت ها قرار دهید اما می توانید ماژول ها را در ماژول های دیگر نیز قرار دهید. این قدرتمند است، اما به راحتی می توان از آن سوء استفاده کرد.

هنگامی که یک ماژول به یک جزء یا یک ماژول دیگر اضافه شد، از قبل در نمودار Dagger قرار دارد. Dagger می تواند آن اشیاء را در آن جزء فراهم کند. قبل از افزودن یک ماژول، بررسی کنید که آیا آن ماژول قبلاً بخشی از نمودار Dagger است و بررسی کنید که آیا قبلاً به مؤلفه اضافه شده است یا با کامپایل کردن پروژه و دیدن اینکه آیا Dagger می تواند وابستگی های مورد نیاز آن ماژول را پیدا کند.

رویه خوب حکم می کند که ماژول ها باید فقط یک بار در یک مؤلفه (خارج از موارد خاص استفاده پیشرفته Dagger) اعلام شوند.

فرض کنید نمودار خود را به این شکل پیکربندی کرده اید. ApplicationComponent شامل Module1 و Module2 و Module1 شامل ModuleX است.

کاتلین

@Component(modules = [Module1::class, Module2::class])
interface ApplicationComponent { ... }

@Module(includes = [ModuleX::class])
class Module1 { ... }

@Module
class Module2 { ... }

جاوا

@Component(modules = {Module1.class, Module2.class})
public interface ApplicationComponent { ... }

@Module(includes = {ModuleX.class})
public class Module1 { ... }

@Module
public class Module2 { ... }

اگر اکنون Module2 به کلاس های ارائه شده توسط ModuleX بستگی دارد. یک روش بد گنجاندن ModuleX در Module2 است زیرا ModuleX دو بار در نمودار گنجانده شده است همانطور که در قطعه کد زیر مشاهده می شود:

کاتلین

// Bad practice: ModuleX is declared multiple times in this Dagger graph
@Component(modules = [Module1::class, Module2::class])
interface ApplicationComponent { ... }

@Module(includes = [ModuleX::class])
class Module1 { ... }

@Module(includes = [ModuleX::class])
class Module2 { ... }

جاوا

// Bad practice: ModuleX is declared multiple times in this Dagger graph.
@Component(modules = {Module1.class, Module2.class})
public interface ApplicationComponent { ... }

@Module(includes = ModuleX.class)
public class Module1 { ... }

@Module(includes = ModuleX.class)
public class Module2 { ... }

در عوض، شما باید یکی از موارد زیر را انجام دهید:

  1. ماژول ها را دوباره فاکتور کنید و ماژول مشترک را به کامپوننت استخراج کنید.
  2. یک ماژول جدید با اشیایی که هر دو ماژول به اشتراک می گذارند ایجاد کنید و آن را در جزء استخراج کنید.

عدم بازسازی به این روش منجر به ایجاد بسیاری از ماژول‌ها می‌شود که بدون داشتن حس سازماندهی واضحی به یکدیگر متصل می‌شوند و دیدن اینکه هر وابستگی از کجا آمده است را دشوارتر می‌کند.

تمرین خوب (گزینه 1) : ModuleX یک بار در نمودار Dagger اعلام می شود.

کاتلین

@Component(modules = [Module1::class, Module2::class, ModuleX::class])
interface ApplicationComponent { ... }

@Module
class Module1 { ... }

@Module
class Module2 { ... }

جاوا

@Component(modules = {Module1.class, Module2.class, ModuleX.class})
public interface ApplicationComponent { ... }

@Module
public class Module1 { ... }

@Module
public class Module2 { ... }

تمرین خوب (گزینه 2) : وابستگی های رایج از Module1 و Module2 در ModuleX به یک ماژول جدید به نام ModuleXCommon که در جزء گنجانده شده است استخراج می شود. سپس دو ماژول دیگر به نام‌های ModuleXWithModule1Dependencies و ModuleXWithModule2Dependencies با وابستگی‌هایی که مختص هر ماژول است ایجاد می‌شود. همه ماژول ها یک بار در نمودار Dagger اعلام می شوند.

کاتلین

@Component(modules = [Module1::class, Module2::class, ModuleXCommon::class])
interface ApplicationComponent { ... }

@Module
class ModuleXCommon { ... }

@Module
class ModuleXWithModule1SpecificDependencies { ... }

@Module
class ModuleXWithModule2SpecificDependencies { ... }

@Module(includes = [ModuleXWithModule1SpecificDependencies::class])
class Module1 { ... }

@Module(includes = [ModuleXWithModule2SpecificDependencies::class])
class Module2 { ... }

جاوا

@Component(modules = {Module1.class, Module2.class, ModuleXCommon.class})
public interface ApplicationComponent { ... }

@Module
public class ModuleXCommon { ... }

@Module
public class ModuleXWithModule1SpecificDependencies { ... }

@Module
public class ModuleXWithModule2SpecificDependencies { ... }

@Module(includes = ModuleXWithModule1SpecificDependencies.class)
public class Module1 { ... }

@Module(includes = ModuleXWithModule2SpecificDependencies.class)
public class Module2 { ... }

تزریق کمکی

تزریق کمکی یک الگوی DI است که برای ساخت یک شی استفاده می‌شود که در آن برخی از پارامترها ممکن است توسط چارچوب DI ارائه شوند و برخی دیگر باید در زمان ایجاد توسط کاربر ارسال شوند.

در اندروید، این الگو در صفحه‌های جزئیات رایج است که در آن شناسه عنصری که باید نشان داده شود فقط در زمان اجرا مشخص است، نه در زمان کامپایل که Dagger نمودار DI را تولید می‌کند. برای کسب اطلاعات بیشتر در مورد تزریق کمکی با Dagger، به مستندات Dagger مراجعه کنید.

نتیجه گیری

اگر قبلاً این کار را نکرده‌اید، بخش بهترین روش‌ها را مرور کنید. برای مشاهده نحوه استفاده از Dagger در یک برنامه Android، به استفاده از Dagger در کد لبه برنامه Android مراجعه کنید.