صفحه مبانی 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' }
Dagger in Android
یک برنامه Android نمونه را با نمودار وابستگی از شکل ۱ درنظر بگیرید.
شکل ۱. نمودار وابستگی کد نمونه
در Android، معمولاً نمودار Dagger ایجاد میکنید که در کلاس برنامه شما قرار دارد زیرا میخواهید تا زمانی که برنامه درحال اجرا است، نمونهای از نمودار در حافظه باشد. به این ترتیب، نمودار به چرخه حیات برنامه پیوست میشود. در برخی موارد، ممکن است بخواهید بافت برنامه نیز در نمودار دردسترس باشد. برای این کار، باید نمودار در کلاس Application باشد. یکی از مزایای این
رویکرد این است که نمودار برای دیگر کلاسهای چارچوب Android دردسترس است.
علاوهبراین، با امکان استفاده از کلاس 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(); }
چون کلاسهای چارچوب Android خاصی مثل فعالیتها و قطعهها توسط سیستم نمونهسازی میشوند، 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 مؤلفههای معماری Android نیست؛ بلکه فقط یک کلاس معمولی است که بهعنوان ViewModel عمل میکند.
برای اطلاعات بیشتر درباره نحوه تزریق این کلاسها، کد را در
پیادهسازی رسمی Android Blueprints Dagger، در
شاخه dev-dagger بررسی کنید.
یکی از ملاحظات مربوط به Dagger این است که فیلدهای تزریقشده نمیتوانند خصوصی باشند. آنها باید حداقل قابلیت رؤیت package-private داشته باشند، مانند کد قبلی.
درحال تزریق فعالیتها
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() را فراخوانی میکنید و نمونهای از فعالیت را که درخواست تزریق میکند ارسال میکنید.
هنگام استفاده از فعالیتها، Dagger را در
روش onCreate() فعالیت قبلاز فراخوانی super.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; } }
پیمانههای Dagger
برای این مثال، از کتابخانه شبکهسازی 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 که توسط Dagger تزریق شده است
نقطه ورودی به گراف LoginActivity است. چون LoginActivity
LoginViewModel را تزریق میکند، Dagger نموداری میسازد که میداند چگونه نمونهای از LoginViewModel و بهصورت بازگشتی، وابستگیهای آن را ارائه دهد. Dagger بهدلیل حاشیهنویسی @Inject در سازنده کلاسها میداند چگونه این کار را انجام دهد.
درون ApplicationComponent تولیدشده توسط Dagger، یک روش از نوع کارخانه وجود دارد تا نمونههایی از تمام کلاسهایی که میداند چگونه ارائه دهد را دریافت کند. در این
مثال، Dagger به NetworkModule موجود در
ApplicationComponent واگذار میکند تا نمونهای از LoginRetrofitService را دریافت کند.
حوزههای Dagger
در صفحه مبانی 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() { ... } }
مراقب باشید هنگام اعمال کردن دامنهها به اشیاء، نشت حافظه ایجاد نشود. تا زمانی که مؤلفه محدودشده در حافظه باشد، شیء ایجادشده نیز در حافظه است. چون ApplicationComponent هنگام راهاندازی برنامه (در کلاس Application) ایجاد میشود، وقتی برنامه ازبین میرود، آن هم ازبین میرود. بنابراین،
نمونه منحصربهفرد UserRepository همیشه در حافظه باقی میماند تا زمانی که
برنامه ازبین برود.
زیرمؤلفههای Dagger
اگر جریان ورود به سیستم شما (که توسط یک LoginActivity مدیریت میشود) از چند قطعه تشکیل شده است، باید از همان نمونه LoginViewModel در همه قطعهها استفاده کنید. @Singleton نمیتواند LoginViewModel را برای استفاده مجدد از نمونه حاشیهنویسی کند
به دلایل زیر:
نمونه
LoginViewModelپساز اتمام جریان در حافظه باقی میماند.برای هر جریان ورود به سیستم، نمونه متفاوتی از
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 است، باید آن را با این روش نشان دهید:
ایجاد یک ماژول 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 { }
افزودن واحد جدید (یعنی
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را ایجاد کنند. مؤلفه والد باید روشی را در میانای خود اضافه کند تا به مصرفکنندگان اجازه دهد نمونههایی از مؤلفه فرعی را از نمونهای از مؤلفه والد ایجاد کنند:Expose the factory that creates instances of
LoginComponentin the interface:کاتلین
@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 را بین قطعههای مربوط به ورود به سیستم همرسانی میکردید. اما همچنین، هر زمان که جریان ورود به سیستم جدیدی وجود دارد، نمونههای متفاوتی از 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 دوباره استفاده کرد. به همین دلیل است که باید آن را @ActivityScope بنامید
نه @LoginScope.
کاتلین
// 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) } }
شکل ۳ نشان میدهد که نمودار Dagger با زیرمؤلفه جدید چگونه بهنظر میرسد. کلاسهای
دارای نقطه سفید (UserRepository، LoginRetrofitService، و LoginViewModel)
کلاسهایی هستند که نمونهای منحصربهفرد در محدوده مؤلفههای مربوطهشان دارند.
شکل ۳. نمایش گراف ساختهشده برای نمونه برنامه Android
بیایید بخشهای نمودار را تجزیه کنیم:
NetworkModule(و بنابراینLoginRetrofitService) درApplicationComponentگنجانده شده است زیرا آن را در مؤلفه مشخص کردهاید.UserRepositoryدرApplicationComponentباقی میماند زیرا در محدودهApplicationComponentاست. اگر پروژه رشد کند، میخواهید همان نمونه را در ویژگیهای مختلف (مثلاً ثبتنام) همرسانی کنید.چون
UserRepositoryبخشی ازApplicationComponentاست، وابستگیهای آن (یعنیUserLocalDataSourceوUserRemoteDataSource) نیز باید در این مؤلفه باشند تا بتوانند نمونههایUserRepositoryرا ارائه دهند.LoginViewModelدرLoginComponentگنجانده شده است زیرا فقط کلاسهای تزریقشده توسطLoginComponentبه آن نیاز دارند.LoginViewModelدرApplicationComponentگنجانده نشده است زیرا هیچ وابستگیای درApplicationComponentبهLoginViewModelنیاز ندارد.بهطور مشابه، اگر
UserRepositoryرا بهApplicationComponentمحدود نکرده بودید، Dagger بهطور خودکارUserRepositoryو وابستگیهای آن را بهعنوان بخشی ازLoginComponentاضافه میکرد زیرا درحالحاضر تنها جایی است کهUserRepositoryاستفاده میشود.
Apart from scoping objects to a different lifecycle, creating subcomponents is a good practice to encapsulate different parts of your application from each other.
ساختاردهی برنامه برای ایجاد زیرگرافهای مختلف 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 سفارشی استفاده میشود که از آن برای
اجرای آزمایشهای ابزار دقیق استفاده خواهید کرد. برای اطلاعات بیشتر درباره این موضوع، کدلب
استفاده از Dagger در برنامه Android را بررسی کنید.
کار کردن با ماژولهای Dagger
ماژولهای 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 { ... }
درعوض، باید یکی از کارهای زیر را انجام دهید:
- ماژولها را بازسازی کنید و ماژول مشترک را به مؤلفه استخراج کنید.
- ماژول جدیدی با اشیایی که هر دو ماژول همرسانی میکنند ایجاد کنید و آن را به مؤلفه استخراج کنید.
Not refactoring in this way results in a lot of modules including each other without a clear sense of organization and making it more difficult to see where each dependency is coming from.
روش خوب (گزینه ۱): 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 { ... }
روش خوب (گزینه ۲): وابستگیهای مشترک از 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 ارائه شود و برخی دیگر باید در زمان ایجاد توسط کاربر ارسال شود.
در Android، این الگو در صفحههای جزئیات رایج است که شناسه عنصر برای نمایش فقط در زمان اجرا مشخص است، نه در زمان ترجمه که Dagger نمودار DI را تولید میکند. برای کسب اطلاعات بیشتر درباره تزریق کمکی با Dagger، به اسناد Dagger مراجعه کنید.
نتیجهگیری
اگر قبلاً این کار را نکردهاید، بخش روالهای مطلوب را مرور کنید. برای دیدن نحوه استفاده از Dagger در برنامه Android، به استفاده از Dagger در codelab برنامه Android مراجعه کنید.