نگاهی عمیق‌تر به Dagger – بخش دوم

dagger 2 android tutorial آموزش اندروید فارسی دگر 2 دیزاین پترن تزریق وابستگی dependency injection

سطح آموزش:  #پیشرفته

پیش‌نیاز

برای مطالعه این آموزش نیاز دارید با Dependency Injection design Pattern آشنا باشید. همچنین باید دو پست قبلی (۱ ۲) رو هم مطالعه کرده باشید.

نگاهی عمیق‌تر به Dagger 2 – بخش دوم

تو بخش قبلی دیدیم که چطور پروژمون رو براساس dagger بسازیم و حسابی با component و module سر و کله زدیم.

این بخش رو با طرح یک مشکل شروع می‌کنم. داخل onCreate توی کلاس DaggerTutorialApplication کدهای زیر رو اضافه میکنم:

خروجی شکل زیر رو پیدا میکنه.

مشکل اینه که ما به ازای هر باری که picasso رو صدا کردیم یک شی جدید تولید شده برامون.

توی ساختار ماژولار پروژه ما بارها یک شی رو استفاده می‌کنیم. برای مثال ممکنه context بارها توی moduleهای مختلف مورد استفاده قرار بگیره و ما توقع داریم هر بار از همون context اولیه استفاده بشه. یا مثلا میخوایم وقتی کلاینت رتروفیت رو در جاهای مختلف استفاده می‌کنیم در تمام مدت از یک کلاینت استفاده کنیم.

برای حل این مشکل مفهومی تحت عنوان Scope مطرح شده که بایستی ازش استفاده کرد.

آموزش استفاده از Scopes در Dagger 2

با توجه به طرح مشکلی که بالاتر کردم هدفمون اینه که هرجا componentـی تعریف کردیم مادامی که از اون component استفاده می‌کنیم از اشیائمون تنها یک نسخه ساخته بشه.

Scope معنی محدوده و قلمرو میده. وقتی برای یک module محدوده تعریف می‌کنیم میگیم که ماژول تو محدود به این component هستی و مادامی که توی این محدوده قرار داری حق داری تنها یک نسخه از خودت بسازی و باقی موارد فقط اون مقدار رو استفاده کنی.

پس scope بیشتر شبیه به یک گزاره‌ست که به ماژول میگه تو چه حیطه‌ای فعالیت کنه. بنابراین با یک annotation میشه scope ساخت.

به شکل زیر من یک Scope برای کلاس application می‌سازم:

حالا باید به component و module وجود scope رو اطلاع بدم. این کار خیلی ساده‌ست فقط کافیه بالای کلاس component انوتیشن @scope رو قرار بدیم.

پس component ما به شکل زیر تغییر پیدا می‌کنه:

همچنین باید به moduleها هم از وجود scope اطلاع بدیم. برای این کار کافیه به بالای هر متدی که @provide داره @scope رو که اینجا @DaggerTutorialApplicationScope هست رو اضافه کنیم. مثلا PicassoModule به شکل زیر تغییر میکنه:

تنها تغییری که این دو کلاس کردن اضافه شدن @DaggerTutorialApplicationScope بهشون بوده.

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

می‌بینید که هر سه شی picasso یکی هستن.

 چرا از @singleton استفاده نکردیم؟

یک سوالی که احتمالا براتون پیش اومده اینه که چرا مثل اولین آموزش dagger از سینگلتون استفاده نکردیم؟ تو جواب باید بگم که singleton هم یک scope هست  واینجا میشه گفت مشکلی پیش نمیومد اگر از singleton هم استفاده می‌کردیم. اما کاری که singleton میکنه (مراجعه کنید به تعریف دیزاین پترن سینگلتون) اینه که مطمئن میشه ما فقط و فقط یک دونه از این شیء در سراسر برناممون داریم. در صورتی که ما نمیخوایم مثلا هرجا که context داریم همین context رو استفاده کنیم. یا نمیخوایم هرجا retrofit خواستیم از همین شیء قبلا ساخته شده استفاده کنیم. ما فقط میخواستیم مادامی که داریم از این component بخصوص استفاده می‌کنیم از اشیاء یکی داشته باشیم.

پس اگر بخوام خلاصه کنم Scope ایجاد اشیاء رو محدود به هر کامپوننت میکنه. مطمئن میشه که مادامی که داریم از این component استفاده می‌کنیم تنها یک شیء از اونچه بهش نیاز داریم تولید بشه.

آموزش استفاده از Qualifier در ۲ Dagger

فرض کنید ما دوتا service داریم که هر دو احتیاج به یک شیء از retrofit دارن. یک چیزی شبیه به تکه کد زیر:

توی این مثال ما دوتا retrofit داریم یکی برای ارتباط با api یاهو و دیگری برای گوگل. حالا providerها از کجا تشخیص میدن که برای هر وبسرویس به سراغ کودوم کلاینت رتروفیت باید برن؟

جواب اینه که ما باید با استفاده از Qualifierها براشون مشخص کنیم.

من کدهای پروژه رو به شکل زیر تغییر میدم. یک anotherApiService و anotherRetrofit می‌سازم تا به عمد در ساز و کار dagger مشکل ایجاد کنم.

اگر سعی کنیم این کد رو build کنیم خطای زیر رو می‌گیریم:

Error:(22, 16) error: ir.coursio.daggertutorial.api.ApiService is bound multiple times:
@Provides @ir.coursio.daggertutorial.scopes.DaggerTutorialApplicationScope ir.coursio.daggertutorial.api.ApiService ir.coursio.daggertutorial.modules.ApiServiceModule.apiService(retrofit2.Retrofit)
@Provides @ir.coursio.daggertutorial.scopes.DaggerTutorialApplicationScope ir.coursio.daggertutorial.api.ApiService ir.coursio.daggertutorial.modules.ApiServiceModule.anotherApiService(retrofit2.Retrofit)

حل مشکل

برای این رفع مشکل یک qualifier تعریف می‌کنیم. qualifierها هم از جنس annotation هستن بنابراین با @interface تعریف میشن.

من یک Qualifier برای ApiService اصلی به این شکل میسازم:

و یک Qualifier برای Retrofit اصلیم به این شکل:

حالا کافیه هرجا که احتیاج به خروجی دارم از qualifier استفاده کنم. پس باید ApiServiceModuleـم به این شکل در بیاد:

همچنین component هم چون از ApiService استفاده می‌کنه به شکل زیر میشه:

توجه کنید که در اصل من باید چهارتا qualifier تعریف می‌کردم. دو تا برای retrofitها و دو تا برای ApiServiceها اما از اونجایی که یکی از این‌ها رو من صرفا برای ایجاد مشکل ساخته بودم و قصد استفاده ازش رو نداشتم برای qualifierـی هم تعریف نکردم.

یک راه حل دیگه برای همین مشکل استفاده از @named هست که من به شخصه ترجیح میدم از qualifierها استفاده کنم. @named یکمی حالت hard code داره و من خیلی خوشم نمیاد. اما اگر از ساختن annotation خوشتون نمیاد می‌تونید از @named هم استفاده کنید. تقریبا توی تمام لینک‌هایی که تو بخش منابع تو پست قبلی گفتم روش استفاده از @named گفته شده.

آموزش Cross Scoping  در Dagger 2

cross scoping در واقع داشتن چندین کامپوننت در سطوح مختلف برنامه‌ست.

حالا چرا اساسا نیاز داریم به همچین چیزی؟

ببینید مفاهیم مختلف توی اندروید (منظورم مثلا application و activity و service و… هست) دارای life cycleهای مختلف هستن. برای همین مثلا هر چیزی که ما توی اکتیویتی استفاده میکنیم رو باید با از بین بردن اکتیویتی از بین ببریم.

پس در واقع ما هرجا که با دو تا مفهوم با lifecycle مختلف برخورد داریم باید componentهای جدا براشون در نظر بگیریم. یعنی مثلا یک فرگمنت و یک اکتیویتی نباید از یک component استفاده کنن. همینجور برای service و application و BroadcastReceiver و ContentProvider.

یکمی کدیزه صحبت کنیم!

پروژمون یک اکتیویتی داره که وظیفه نمایش لیست جوک‌ها رو به عهده داره. فعلا کدهای این اکتیویتی به این شکله:

توضیح کد

MainActivity از BaseActivity ارث بری می‌کنه. توی BaseActivity من اجازه دسترسی رو می‌گیرم. جدا کردنشون کمک میکنه کدهای که توی Activity هست فقط مربوط به همین آموزش باشه. نکته خاصی هم نداره اینجا توضیح دادم چجوری این کارو کنید.

کد ابتدا picasso و apiService رو از کلاس application میگیره و بعد هم RecyclerView و Adapter رو آماده میکنه.

وقتی هم که روی btnGetJoke کلیک میشه یک درخواست برای سرور ارسال میکنه و با اومدن جوک‌ها لیست رو آپدیت میکنه.

حالا من قصد دارم برای MainActivity یک component جدا بسازم تا بعد نحوه ارتباط برقرار کردن بین این component و کامپوننتِ اپلیکیشن رو بررسی کنیم.

برای این‌کار مثل چیزی که تا حالا یاد گرفتیم کلاس MainActivityModule و MainActivityComponent رو می‌سازیم:

حتما متوجه شدید که من از @MainActivityScope هم استفاده کردم.

حالا وقتی بخوایم کد رو کامپایل کنیم تا dagger کارشو انجام بده متوجه میشیم که نمیتونیم! چرا که Picasso رو نداریم. ما توی ماژولمون پیکاسو رو به عنوان ورودی تعریف کردیم ولی هیچ‌جا این وابستگی رو پاسخ ندادیم.میتونیم البته مثل قبل یک متد برای تولید پیکاسو بسازیم ولی ما دوست نداریم این کارو کنیم چون یکبار این کارو کردیم و توقع داریم dagger که انقدر خوب همه چیزو میفهمه اینم بفهمه و خودش درست کنه.

ما قبلا برای اینکه رابطه module و component رو تعریف کنیم اینجوری عمل می‌کردیم:

وقتی می‌گیم module رو اضافه کن یعنی هر چیزی که اون module داره (provides) رو میتونی ازش استفاده کنی.

همین کارو با یک کامپوننت دیگه هم میشه کرد. میگیم:

حالا اگر build رو بزنیم پروژه خیلی راحت از picassoـی که moduleها قبلا برای DaggerTutorialApplicationComponent فراهم کرده بودن استفاده میکنه و پروژه رو می‌سازه.

حالا کافیه به MainActivity بریم و از componentـی که ساختیم استفاده کنیم:

کد

فقط توجه کنید که ما یک وابستگی به DaggerTutorialApplicationComponent ایجاد کردیم و باید این نیاز رو تامین کنیم. برای همین تغییراتی رو داخل کلاس DaggerTutorialApplication میدیم که بتونه component خودش رو برگردونه. این تغییرات خیلی ساده‌ست فقط کافیه یک متد بنویسیم که مقدار component رو برگردونه. در نهایت کلاس DaggerTutorialApplication  به شکل زیر در میاد:

Dagger و معجزه‌ای دیگر

اگر یکمی توجه کرده باشید احتمالا این سوال براتون پیش میاد که حالا که ما میتونیم از Picassoـی که توی سطح application ساختیم استفاده کنیم چرا از ApiServiceش استفاده نمیکنیم؟ اینجوری میتونیم از شر کد زیر هم خلاص شیم:

واقعیت اینه که نه فقط این. بلکه ما توی اپلیکیشنمون تنها چیزی که احتیاج داریم componentمون هست. بقیه چیزها (اینجا ApiService و Picasso) رو میتونیم خیلی راحت از همین component بگیریم. پس کد کلاس اپلیکیشن میشه:

توجه کنید که ما اگر جای دو تا setupـی که داشتیم (برای Api و Picasso) هزارتای دیگه هم میداشتیم باز تعداد خطوط کلاسمون همین می‌بود!

برای کلاس MainActivity هم کافیه اول چیزایی که نیاز داریم رو به component اضافه کنیم:

و سپس کدهای داخل کلاس رو به شکل زیر تقلیل بدیم (فقط onCreate رو می‌نویسم چون بقیه تغییری نمیکنه):

یعنی در حال حاضر تمام نیازهای اپلیکیشن ما داره پشت سر فراهم میشه و ما هرجا به هرچیزی نیاز داشته باشیم فقط کافیه متدش رو (بدون پیاده‌سازی) به interface کامپوننت اضافه کنیم و بعد ازش استفاده کنیم. یعنی برای همیشه میتونیم با initialize کردن خداحافظی کنیم…

دیگه از این بهترم میشه مگه؟

جواب اینه که آره میشه 🙂

 

آموزش Inject  در Dagger 2

رسیدیم به آخرین بخش از آموزش Dagger، جایی که معجزه واقعی اتفاق میفته…

تا الان ما متدهایی که میخواستیم داشته باشیم رو توی component تعریف می‌کردیم و بعد هرجا احتیاج بهشون داشتیم با استفاده از ساختن نمونه از component ازشون استفاده می‌کردیم.

حالا می‌خوام راهی رو نشونتون بدم که ما نه متدهایی که نیاز داریم رو تعریف کنیم و نه با استفاده از component اون‌ها رو صدا بزنیم. به جاش فقط به Dagger میگیم که من این فیلدها رو دارم و نیاز دارم که تو اون‌ها رو مقدار دهی کنی!

چجوری؟

برای این کار از inject@ استفاده میکنیم.

اول از همه باید بگیم که کجا باید عمل inject یا تزریق انجام بشه. گفتیم که احتیاجی به متدهای component نداریم برای همین تمام متدها رو حذف می‌کنیم و جاش فقط میگیم برو به این activity و هر چیزی رو که بهت میگم رو مقداردهی کن.

برای این‌کار کافیه بنویسیم:

و بعد داخل MainActivity می‌نویسیم:

کد

و تمام!

 

دیگه همون یک خطی که می‌نوشتیم:

رو هم احتیاج نداریم. حتی اگر صدتا وابستگی هم داشته باشیم تنها کافیه نوع متغیر رو بنویسیم و اسمی که می‌خوایم رو براش انتخاب کنیم. فقط توجه هم باید داشته باشیم که اگر qualifier داره اون رو هم ذکر کنیم.

در نهایت به ازای تمام خط‌هایی که باید می‌نوشتیم فقط می‌نویسیم:

مثلا اگه احساس کنیم داخل MainActivity به یک picasso احتیاج داریم فقط کافیه بگیم:

این رو مقایسه کنید با گذشته که برای ساخت یک شی باید چقدر به دردسر میفتادید.

چه اتفاقی میفته؟

اتفاقی که در اصل میفته اینه که وقتی ما داخل کامپوننت متد زیر رو اضافه میکنیم، داریم به Dagger میگیم که برو به کلاس MainActivity و هرجا inject دیدی مقدار مناسب رو تزریق کن.

و وقتی داخل اکتیویتی این متد رو فراخونی می‌کنیم ارتباط بین module و وابستگی‌ها رو از طریق کامپوننت برقرار می‌کنیم. و dagger به صورت خودکار هرجایی درخواست تزریق ببینه این کارو انجام میده.

Constructor Inject با Dagger 2

اولین صحبت‌هامون توی پترنِ dependency injection و بعدا توی آموزش dagger راجع به constructor injection بود. این بار ولی به عنوان آخرین بخش بهش می‌پردازیم.

constructor شاید بهترین قابلیت Dagger باشه.

نگاهی به adapterمون بندازیم:

ما توی MainActivity یک وابستگی به شیء Adapter داریم برای همین نیاز داریم که یکبار و داخل یک module یک شیء از JokeAdapter بسازیم و بفرستیم برای کامپوننت تا داخل کلاس MainActivity ازش استفاده کنه. یک مسئله‌ای هم هست مثلا الان اگه بخوایم به constructorـه JokeAdapter یک پارامتر اضافه کنیم باید بریم داخل moduleها و هرجا JokeAdapter می‌سازیم رو تغییر بدیم.

ما این کارو برای Picasso و Cache و File و… هم انجام میدیم.

اما یک تفاوتی بین این دو هست. Picasso و… تحت کنترل ما نیستن در صورتی که JokeAdapter رو ما خودمون ساختیم.

آیا راهی هست که به dagger بگیم constructor ما این شکلی ساخته میشه و بعد ازش بخوایم هرجا نیاز داشت خودش از روی constructor شیء مورد نظر رو بسازه و return کنه؟

جواب اینه که بله هست. با Constructor Injection ما تنها constructor رو تعریف می‌کنیم و با annotation گذاری به Dagger میگیم هر وقت احتیاجی به شیءای از این  جنس داشتی constructorش اینجاست. ورودی‌ها رو هم من برات تامین می‌کنم. خودت بیا و شیء رو بساز.

MainActivityModule به شکل زیر هست:

کد

ما میخوایم دیگه نمونه‌ای از JokeAdapter رو نسازیم. برای این منظور اول به سراغ constructorش میریم و inject@ رو براش میذاریم. همچنین Context رو از جنس MainActivity قرار میدیم:

 

حالا کافیه module رو به شکل زیر تغییر بدیم:

Dagger از این به بعد تمام وابستگی‌هایی که خودمون کلاسش رو نوشته باشیم رو هم خود به خود برامون می‌سازه. تنها کافیه ما Inject@ رو برای constructor بذاریم.

یعنی اگر صد تا کلاس هم داشتیم با همین ۱۵ خط میتونستیم به وابستگی‌شون پاسخ بدیم.

 

پایان

توی این سری آموزش‌ها بیشتر چیزهایی که مهم بودن رو راجع به Dagger یاد گرفتیم. برای مسلط شدن میتونید روی همین پروژه‌ای که برای این سری از آموزش‌ها استفاده می‌کردم استفاده کنید. پروژه رو می‌تونید روی گیت‌هاب ببینید.

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

 

 

 

 

12 دیدگاه برای “نگاهی عمیق‌تر به Dagger – بخش دوم

    1. نه استثنا نیست البته میشه بدون استفاده از کلاس application هم پیاده‌سازی کرد اما سوای استفاده از Dagger این یک شیوه استاندارده که المان‌هایی که توی کل اپلیکیشن نیاز داریم رو توی کلاس اپلیکیشن initialize کنیم.

  1. سلام
    توضیحات قبلیتون رو کاملا موتوجه شده اما این بخش (نگاهی عمیق‌تر به Dagger – بخش دوم) یه کم انگار سنگین تر بود و یه جاهاییشو فهمیدم و یه جاهاییشو نه و یه جاهاییشم گیج شدم اگر مقدوره بخش های دیگری هم بذارید با توضیحات بیشتر تا من کامل این مباحش رو متوجه بشم و بتونم داخل پروژه هام ازش استفاده کنم . خیلی ممنون میشم اگر بخش بیشتری بنویسید .
    تشکر

    1. سلام. در آینده حتما بخش‌های دیگه میذارم ولی فعلا اولویتم نوشتن راجع به kotlinـه. میتونید به لینک‌هایی که داخل پست‌های Dagger معرفی کردم مراجعه کنید.

  2. سلام خسته نباشید, من یه سوال داشتم الان شما کلاس اداپتر رو محدود به این کردین که context ورودیش فقط از طرف MainActivity باشه, اگر بخوایم از اداپتر در کلاس های مختلف استفاده کنیم چجوری باید پیاده سازیش کنیم؟

  3. خیلی خوب که تجربیاتتون رو به اشتراک می گذارید.
    سوالی که برای من پیش اومده اینکه Dagger2 میتونه به صورت Generic هم Injection رو انجام بده؟
    برای مثال :
    @Provides
    public T apiService(Retrofit retrofit, Class aClass) {
    return retrofit.create(aClass);
    }

پاسخ دهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *