نگاهی عمیق‌تر به Dagger

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

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

پی‌نوشت: تصمیم گرفتم یک دسته آموزش جدید تحت عنوان نگاهی عمیق‌تر یا deeper look درست کنم که بعضی مباحث رو یکمی دقیق‌تر و جامع‌تر توش بررسی کنم.

نگاهی عمیق‌تر به Dagger 2

مقدمه

پیش نیاز این آموزش آشنایی با دیزاین پترن Dependency Injection و همچنین مطالعه قسمت قبلی آموزش dagger هست.

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

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

خلاصه اینکه پروژه چطور کار می‌کنه رو فراموش کنید و به اینکه چی کار می‌کنه توجه کنید.

منابع

برای آماده کردن این مجموعه آموزش از منابع مختلفی استفاده کردم که تو پاراگراف بعدی میتونید لینک‌هاشون رو ببینید. بخصوص این مجموعه که بخشی از کدها رو هم ازش قرض گرفتم نقش زیادی توی این مجموعه مقالات و یادگیری خودم داشته.

+ + + + + + + + + + + +

 

 شروع به کار، آشنایی با پروژه

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

پیکربندی پروژه ولی یک مقداری با همیشه تفاوت میکنه. فعلا خیلی نیازی نداریم وارد جزئیات کل پروژه بشیم. بذارید ابتدا نگاهی به کلاس Application پروژه بندازیم:

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

ApiService یک سرویس رتروفیتی هست برای ارتباط با سرور.

داخل onCreate ابتدا timber رو داریم. Timber یک کتابخونه مثل Log در اندروید هست که فعلا کاری بهش نداریم و ارتباطی به dagger نداره. تنها نکته‌ش اینه که timber توی نسخه release لاگ نمایش نمیده.

HttpLoggingInterceptor کارش اینه که درخواست‌های Http و پاسخ‌هایی که برمیگردونه رو برامون لاگ میکنه.

یک فایل داریم که اطلاعات دریافتی از اینترنت رو اونجا cache میکنیم. بعد هم یک cache میسازیم که تا ۵ مگابایت اطلاعات رو برامون کش کنه.

در ادامه یک clientـه okhttp ساختم. یک شی picasso برای لود عکس و یک کلاینت رتروفیت برای انجام درخواست‌ها.

چرا Dependency Injection؟

احتمالا با خوندن دو تا پست قبلی یا به هر دلیل دیگه که نیاز به استفاده از dependency injection رو حس کردید و میدونید که کدی که باهاش روبرو هستیم اصلا مدل جالبی نداره.

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

ممکنه کسی بگه من هرجا خواستم از picasso یا service استفاده کنم همونجا نمونه‌گیری می‌کنم. اما فرض کنید شما توی هر کلاسی صدها خط کد رو اضافه کنید برای اینکه وابستگی‌هاتون کم بشه (چون به این شکل نمیشه وابستگی رو صفر کرد)

اگر اهل نوشتن تست برای برنامه‌هاتون باشید (یا بخواید در آینده اهلش بشید!) میدونید که چنین وابستگی‌هایی تست نوشتن رو خیلی سخت و تو مواردی غیر ممکن میکنن.

بذارید یک مقداری روی کد صحبت کنیم.

اول خروجی پروژه نگاه کنیم:

با کلیک روی GET JOKES لیستی از جوک‌ها از سرور گرفته میشه و در لیست نمایش داده میشه. من برای اینکه یک مقداری راحت‌تر توضیح بدم یک عکس هم به پروژه اضافه کردم که البته بخشی از وب سرویس نیست و لوگوی coursioـه که از یک آدرس جدا میخونم ولی میتونیم فرض کنیم که لیستمون یک عکس هم داره که با picasso لود میشه.

کلاس Adapter شکل زیر رو داره:

کد

توجه کنید که Picasso اینجا به این شکل صدا شده:

ایرادی که این روش داره اینه که مثلا من دوست دارم از دانلودر okhttp3 برای دانلود تصاویر استفاده بشه. برای این‌کار همونطور که بالا دیدید همچین کاری لازمه:

اما از طرفی من دوست ندارم هرجا که load عکس دارم به شکل بالا عمل بکنم. این وابستگی من به شی پیکاسو برای لود تصویر رو باید به شکلی حل بکنم. برای همین از Dependency Injection استفاده میکنم و در نهایت کد کلاس adapter به شکل زیر میشه (برای ساده‌تر شدن من قسمت‌هایی که تغییر دادم رو می‌نویسم فقط):

کد

الان من اون مشکل رو حل کردم. فقط الان بایستی به هرجا که adapter رو صدا کردم برم و یک شیء پیکاسو به شکلی که دوست دارم (مثلا با دانلودر okhttp3) رو بسازم و به کلاس adapter تزریق کنم.

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

حالا کافیه توی هر activity که نیاز به پیکاسو داریم به شکل زیر عمل کنیم:

 

و بعد به این شکل adapter رو بسازیم:

بذارین تا اینجای پروژه یک دیاگرام از وابستگی‌هامون رسم کنم:

android dagger dependency diagram

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

اگر قرار باشه چندین سرویس مختلف داشته باشیم، وابستگی اکتیویتی و فرگمنت و.. رو اضافه کنیم، managerها و handlerها رو به دیاگرام اضافه کنیم و… میتونید تصور کنید که چقدر راحت کنترل این وابستگی‌ها میتونه از دستمون خارج شه. اینجاست که Dagger به کمکمون میاد.

Dagger تمام کدی که برای کنترل این وابستگی‌ها نیاز داریم رو برامون میسازه. تنها کافیه بهش بگیم چطوری یه کلاس رو بسازه و کجا به یک instance از اون کلاس نیاز داریم. باقی کارها رو dagger انجام میده و ما بدون نگرانی از اینکه اون پشت چه اتفاقی میفته میتونیم از اشیاء ساخته شدمون استفاده کنیم.

اضافه کردن Dagger به پروژه

تا الان دیدید که برای یک ارتباط با اینترنت چندین خط کد کثیف رو پشت سر هم توی کلاس application قطار کردم. بعد هم سراغ adapter رفتم و نشون دادم که برای استفاده از Picasso باید یک شیء پیکاسو رو بهش پاس بدیم (DI) ولی همین پاس دادن باعث میشه ناچار شیم جاهای مختلف پروژه بگردیم و تغییراتی رو توی کد بدیم.

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

حالا میخوام با استفاده از dagger این پروژه کثیف رو سر و سامون بدم. برای این کار اول سراغ component میریم و یک کلاس component می‌سازیم:

ابتدا به امر یک اینترفیس کامپوننت برای کلاس application میسازم و اسمش رو میذارم DaggerTutorialApplicationComponent :

اتفاقی که الان میفته اینه که dagger میاد متدهای این اینترفیس رو برای ما پیاده‌سازی میکنه. از کجا می‌فهمه؟ ما با استفاده از @component بهش گفتیم.

اینکه چجوری اینکارو میکنه خیلی به ما ارتباطی نداره ولی بد نیست یکمی راجع به نحوه کارکردش هم صحبت کنیم. در عمل میاد به مقدار خروجی نگاه میکنه و متوجه میشه باید یک شیء پیکاسو بسازه. اما ممکنه برای ساختن پیکاسو به objectهای دیگه هم نیاز داشته باشه. تا چند دقیقه دیگه راجع به این صحبت می‌کنیم.

همچنین ممکنه ما چندتا شیء picasso بخوایم تولید کنیم که با هم تفاوت میکنن. مثلا یکی از okhttp3 downloader استفاده میکنه و یکی نمیکنه. این رو چجوری میفهمه؟ این رو هم تو پست‌های بعدی راجع بهش صحبت می‌کنیم. خود dagger نمیتونه بفهمه و ما هستیم که باید به شیوه‌ای که یاد خواهیم گرفت این رو بهش بگیم.

خیلی از بحثمون دور نشیم. گفتم که dagger میاد و picasso و ApiServiceـی که ما احتیاج داریم رو برامون فراهم میکنه. اما از کجا؟

هر شیء پیکاسو ویژگی‌هایی داره dagger از کجا میفهمه ما Picasso رو با چه پارامترهایی میخوایم؟

جوابش خیلی ساده‌ست. خودمون بهش میگیم. اما کجا؟ توی module.

ساخت Module ها برای Dagger

ابتدا یک module برای ApiService می‌سازیم:

ما یک ماژول ساختیم که ApiService رو برامون فراهم کنه. اما یک مسئله‌ای هست. متد apiService برای ساختن یک نمونه از کلاسش نیاز به یک instance از retrofit داره.

همینطور وقتی نگاهی به retrofit بکنیم می‌بینیم که retrofit نیاز به یک کلاینت OkHttp داره و همینجوری این تسلسل تا پایه‌ی گراف وابستگی ادامه پیدا می‌کنه.

Dagger خیلی خوب از پس این مسئله هم بر میاد کافیه ما متدهایی رو بسازیم وابستگی‌های متدهای دیگه رو پاسخ میده. مثلا یک متد بسازیم که رتروفیت برمیگردونه و یک متد که okHttpClient برمیگردونه و…

فقط برای این کار باید اصولی رو که توی الگوی طراحی Dependency Injection گفتم رعایت کنیم.

پس اول متد apiService رو به این شکل تغییر میدیم:

حالا متد رتروفیت رو اضافه میکنیم:

باید متوجه شده باشید که الان هم مجددا برای فراهم کردن OkHttpClient برای متد retrofit نیاز به یک متد دیگه داریم. این زنجیره ادامه پیدا می‌کنه تا ما به تمام وابستگی‌ها پاسخ بدیم. فقط من برای تمیزتر شدن کد وابستگی‌هایی که مربوط به OkHttpClient هست رو داخل یک module جدا به اسم NetworkModule قرار میدم و در نهایت کد این دو Module به شکل زیر میشه:

NetworkModule

اگر به کد دقت کنید می‌بینید که یک Module دیگه به اسم ContextModule هم به پروژه اضافه شده. داخل NetworkModule برای ساخت فایل ما احتیاج به Context داریم و میدونید که context رو نمیشه تولید کرد.

به این دسته از وابستگی‌ها External Dependency یا وابستگی خارجی میگن. برای استفاده از context در dragger یک module دیگه می‌سازیم و کدها زیر رو می‌نویسیم:

چون context رو نمیتونیم تولید کنیم توی constructor این module مقدار دهی میکنی. به این شکل dagger هرجایی احتیاج به context داشته باشه میتونه احتیاجش رو برطرف کنه فقط فراموش نکنید جایی که احتیاج به context دارید ماژول مربوطه رو include کنید:

پس ما moduleهایی که نیاز داشتیم رو ساختیم.

حالا component ما احتیاج داره بدونه این moduleها وجود دارن و اینکه بدونه از کودوما میتونی استفاده کنه. برای این کار annotation بالای کلاس component رو به شکل زیر تغییر میدیم:

توجه کنید که کامپوننت احتیاجی به context یا NetworkModule نداره. این ApiServiceModule هست که به NetworkModule احتیاج داره (یا PicassoModule) و برای رفع نیاز خودش با استفاده از include ماژول نتورک رو صدا کرده قبلا.

حالا کافیه پروژه رو build کنیم و….

💥💥💥💥💥

dagger تمام کلاس‌هایی که لازم داریم رو برامون ساخت 🙂

استفاده از معجزه Dagger

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

تمام کدهایی توی کلاس DaggerTutorialApplication داشتیم رو پاک میکنم و جاش همین چند خط رو میذارم:

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

اما باز هم میشه کد رو از این ساده‌تر کرد. واقعیت اینه که Dagger خودش هم میتونه بفهمه که به NetworkModule نیاز داره، پس چرا خودش برامون نمیسازه؟

به هرحال ممکنه در آینده که تعداد خیلی زیادی module داریم دنبال کردنشون و اضافه کردنشون سخت و پیچیده شه.

جواب اینه که بله Dagger میتونه بسازه و این کارو هم میکنه. در واقع ما احتیاجی به new کردن این moduleها نداشتیم. تنها moduleـی که باید new کنیم ContextModule هست که گفتم External Moduleـه و به یک چیز خارجی (اینجا Context) احتیاج داره. به همین دلیل ما احتیاج داریم این نیاز رو برطرف کنیم. پس در نهایت کد کل کلاس application ما به شکل زیر در میاد:

نتیجه‌گیری

بحث Dagger هنوز تموم نشده. تا اینجا بصورت عملی کاربر Dagger رو توی یک پروژه واقعی دیدیم ولی هنوز هم میتونیم بهتر عمل کنیم. اما چون این آموزش خیلی طولانی شد بقیه رو توی یک آموزش دیگه میگم.

ولی همینقدری هم که یاد گرفتیم کافیه تا بتونیم مزیت‌های Dagger رو نسبت به روش قدیمی بفهمیم. فرض کنید الان شما بخواید تغییری توی HttpLoggingInterceptor بدید. الان تنها باید یکبار و یک جا این تغییر رو اعمال کنید. اما در صورتی که Dagger نبود و خودتون dependency injection رو نوشته بودید باید توی تمام این تابع‌هایی که الان dagger بصورت خودکار درست می‌کنه و می‌رفتید و تک‌تک تغییر می‌دادید (بگذریم که نوشتن این تابع‌ها چقدر وقتتون رو می‌گرفت). اگر DI نداشتید هم که هیچی باید تک‌تک جاهایی که HttpLoggingInterceptor داشتید رو پیدا می‌کردید و تغییر می‌دادید.

و این تازه یکی از فواید Dagger هست.

میدونم دیوونتون کردم انقدر تعریف کردم از Dagger :))

آموزش بعدی وارد مباحث عمیق‌تر و یکمی سخت‌تر dagger میشیم و یکمی حرفه‌ای‌تر از dagger استفاده می‌کنیم.

پی‌نوشت: من از عمد سورس کد این بخش رو در آپلود نکردم. در نهایت که مجموعه آموزش‌های dagger تموم شد همه چیز رو تو قالب یک پروژه روی github قرار میدم.

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

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

  2. واقعا عالی بود.
    دوروزه دارم درباره ی پترن DI فیلم میبینم و مطلب میخونم.آخرم چیز زیادی دستگیرم نشد.
    اما این مجموعه مطالب واقعا عالی بودن به این شکل که در یک سناریوی واقعی ابتدا مشکل رو بیان کردید و بعد step by step اون رو به کمک dagger حل کردید خیلی به درک جزییات کمک کرد.
    فقط نکته ای که فکر میکنم فراموش کرده بودید ساخت PicassoModule بود که از کدهای گیت هاب چکش کردم.

    منتظر قسمت بعدی که فکر میکنم درباره scope هاست، هستیم.

  3. درود بر شما.سال جدید مبارک.
    آموزش عالی بود.
    امکانش هست روش تزریق presenter در معماری MVP توسط دگر رو آموزش بدید.من چنتا سرس از گیت دان کردم اما هرکدوم یک روشی داره که کلا همون قدر هم که یاد داشتم هم فراموش کردم.ممنون می شم آموزش رو تهیه بفرمایید

پاسخ دهید

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