شروع کار با Dagger

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

اجزاء Dagger

همان‌طور که در مطلب «آشنایی با Dagger» اشاره کردم، در «تزریق وابستگی» ما قصد داریم نیازمندی‌های یک کلاس را برآورده کنیم. کلاس Phone را به عنوان یک کلاس وابسته در نظر بگیرید؛ برای تزریق وابستگی Dagger باید بداند:

۱. کلاس Phone به چه کلاس‌هایی وابسته است؟
۲. وابستگی‌های کلاس Phone به چه شکلی باید تأمین/ایجاد شوند؟

با در نظر داشتن سوالات بالا، اکنون به طور اجمالی به اجزای Dagger اشاره می‌کنم. اگر بخشی از توضیحات زیر را متوجه نشدید جای نگرانی نیست، به خواندن ادامه دهید!

  • Inject@: به وسیله این annotation، ما نیازمندی‌های یک کلاس را به Dagger معرفی می‌کنیم. وقتی Dagger به علامت Inject@ می‌رسد، وابستگی‌های کلاس را شناسایی می‌کند. هدف از استفاده از این annotaion، در واقع پاسخ دادن به پرسش ۱ می‌باشد.
  • Provides@: وقتی که بخواهیم یک وابستگی را تأمین کنیم، از Provides@ استفاده می‌کنیم. هر متودی که با Provides@ علامت زده شود، به Dagger اعلام می‌کند که قادر است یک وابستگی خاص را تأمین نماید. درون این متود، ما تصمیم می‌گیریم که به شیوه دلخواهمان، شیء موردنیاز را ایجاد کنیم و در واقع به نوعی به پرسش ۲ پاسخ می‌دهیم.
  • Module@: متـودهای تـأمین‌کننــده (Provides@)، بایــد درون کلاســی قرار بگیرنـــد که با Module@ علامت زده شده است. در یک ماژول، ما معمولاً اشیائی را تأمین (Provides@) می‌کنیم که به نوعی به هم ارتباط دارند و یک مجموعه معنادار را می‌سازند.
  • Component@ اکنون Dagger هم وابستگی‌ها را می‌شناسد و تأمین‌کنندگان آن‌ها را. فقط جای یک جزء دیگر خالی است؛ جزئی که دست تأمین‌کننده را در دست کلاس وابسته بگذارد. این کار توسط کامپوننت‌ها (Component@) انجام می‌شود. کامپوننت‌ها بین Inject@ و Module@ ارتباط برقرار کرده و عمل تزریق را انجام می‌دهند.

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

  • Singleton@: کاربرد این یک مورد دیگر خیلی واضح است؛ وقتی که از این انوتیشن استفاده کنیم، منظورمان این است که فقط یک نمونه از شیء مورد نظر ساخته شود. Dagger هم واقعاً فقط یک نمونه از اشیاء Singleton می‌سازد، با این حال این‌که چه‌طور از این قابلیت استفاده کنیم، اهمیت زیادی دارد! هر شیء Singleton، فقط در کامپوننت (گراف اشیاء) مربوط به خود Singleton است. یعنی اگر ما کامپوننتی را برای تأمین کردن نیازهای یک Fragment بسازیم و در آن از Singleton@ استفاده کنیم، شیء موردنظرمان درون آن Fragment «یکتا» خواهد بود. درک دقیق این موضوع می‌تواند شما را از برخی از ابهامات خلاص کند.

به کارگیری Dagger

بعد از آشنایی با اجزای اصلی Dagger، وقت آن است که وارد عمل بشویم.

راه‌اندازی

ابتدا خط زیر را به به فایل build.gradle اصلی (ریشه) اضافه کنید.

dependencies {  
     // other classpath definitions here
     classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
}

‍‍‍‍‍‍‍‍ سپس خط زیر را به فایل app/build.gradle اضافه کنید تا بتوانید از پلاگینی که در بالا اضافه کردید، استفاده کنید.

apply plugin: 'com.neenbedankt.android-apt'  

‍‍‍‍‍‍‍‍ در نهایت وابستگی‌های زیر را هم به فایل app/build.gradle اضافه کنید. این خطوط باید بعد از عبارتی که با apply شروع می‌شود، بیایند:

dependencies {  
    apt 'com.google.dagger:dagger-compiler:2.7'
    compile 'com.google.dagger:dagger:2.7'
    provided 'javax.annotation:jsr250-api:1.0'
}

مثال

قطعه کد زیر از کلاس MainActivity استخراج شده است. این کد بدون بهره‌گیری از DI یا تزریق وابستگی نوشته شده است.

private void initialize(){  
    netThreadPool = Application.getNetThreadPool();
    bgThreadPool = Application.getNetThreadPool();
photoManager =  
 new PhotoManager(getActivity(), 
netThreadPool, bgThreadPool);

    imageLoader = new ImageLoader(getActivity(), photoManager);

    gson = new GsonBuilder().create();
    converterFactory = GsonConverterFactory.create(gson);
}

به کمک DI می‌توان از راه‌اندازی وابستگی‌هایی (اشیائی) که در مثال بالا می‌بینید، پرهیز کرد. به بیان بهتر، MainActivity می‌تواند از همه اتفاقات بالا، بی‌خبر باشد.

اول وابستگی‌ها را تعریف کنم یا تأمین‌کنندگان را ؟

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

  • مرحله ۱: ابتدا ماژولی را تعریف می‌کنم که برخی از نیازمندی‌های برنامه را تأمین می‌کند
  • مرحله ۲: وابستگی‌ها را مشخص می‌کنم (اهداف تزریق)
  • مرحله ۳: در نهایت کامپوننتی را تعریف می‌کنم که عمل تزریق را انجام می‌دهد

مرحله ۱: singletonهای واقعی

در ابتدای کار با Dagger، به سراغ singletonهایِ واقعی می‌رویم. منظور من از singletonهای واقعی، وابستگی‌ها یا اشیائی هستند که فقط یک نمونه از آن‌ها در کل برنامه وجود دارد. مثلاً اگر قرار باشد تمامی کارهای پس‌زمینه را توسط یک Thread یا ریسه انجام دهیم و همه Activityها به آن دسترسی داشته باشند، باید آن ریسه را با چرخهٔ حیات Application گره بزنیم. بنابراین ماژولی به نام AppModule می‌سازیم و در آن وابستگی‌هایی را تأمین می‌کنیم که singleton واقعی هستند.

@Module
public class AppModule{

  Application app;
  public AppModule(Application app) {
      this.app = app;
  }

  @Provides @Singleton
  FontCache provideFontCache() {
    return new FontCache(app);
  }

  @Provides @Singleton @Named("Network")
  ExecutorService provideNetThreadPool() {
     return Executors.newSingleThreadExecutor();
  }

  @Provides @Singleton @Named("Background")
  ExecutorService provideBgThreadPool() {
    return Executors.newSingleThreadExecutor();
  }

  @Provides @Singleton
  PhotoManager providePhotoManager(@Named("Network") ExecutorService network,@Named("Background") ExecutorService background) {
    return new PhotoManager(app.getApplicationContext(), network, background);
  }

⋮
}

متودها را به شکل دلخواه می‌توان نام‌گذاری کرد. چیزی که اهمیت دارد، نوع داده‌ای است که متودها بر می‌گردانند. Dagger با توجه به داده‌ای که برگردانده می‌شود، تشخیص می‌دهد که هر وابستگی را به وسیله چه متودی می‌تواند تأمین کند.

گاهی لازم است در یک ماژول، چند وابستگی را تأمین کنیم که از یک نوعِ واحد هستند. در این شرایط، Dagger برای تأمین کردن یک وابستگی، دچار سردرگمی خواهد شد. مثلاً در قطعه کد بالا، هر دو متود provideNetThreadPool و provideBgThreadPool، یک شیء از نوع ExecutorService را برمی‌گردانند. به همین خاطر ما از یک انوتیشن توصیف‌کننده (Qualifier) به نام Named@ استفاده کرده‌ایم. وقتی که Dagger بخواهد یک شیء PhotoManager بسازد، به سراغ متود providePhotoManager می‌رود. دو انوتیشن ("Named("Background@ و ("Named("Network@ به Dagger نشان می‌دهند که برای تأمین کردن هر یک از وابستگی‌ها، باید به سراغ کدام متود برود.

اگر بیش‌تر توجه کنید، اتفاقات جالبی در حال رخ دادن است. دو متود provideNetThreadPool و ‍provideBgThreadPool در ماژول بالا، فعلاً استفاده داخلی دارند و وابستگی‌های متود providePhotoManager‍ را تأمین می‌کنند (هنوز وارد مرحله ۳ نشده‌ایم). Dagger به طور خودکار، ترتیب ایجاد شدن گراف اشیاء را مدیریت می‌کند.

نکته: Named@ یک انوتیشن توصیف کننده است که به طور پیش‌فرض در Dagger تعریف شده است. شما می‌توانید با استفاده از انوتیشن Qualifier@، انوتیشن‌های توصیف‌دهنده دلخواه خودتان را بسازید (در این‌جا به این موضوع نمی‌پردازم).

مرحله ۲: اهداف تزریق

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

  public class MainActivity extends Activity {
    @Inject @Named("Network")
    ExecutorService mNetwork;
    @Inject 
    PhotoManager mPhotoManager;

public void onCreate(Bundle savedInstance) {  
     // assign singleton instances to fields
     InjectorClass.inject(this);
  } 
    ⋮
}

کلاس MainActivity‌ در این‌جا دو وابستگی دارد. باز هم از توصیف‌کننده ("Named("Network@ استفاده کرده‌ایم تا Dagger متود provideNetThreadPool را انتخاب کند.

اگرچه Dagger حالا می‌تواند اهداف تزریق را شناسایی کند، با این حال از آن جایی که به کلاس MainActivity دسترسی ندارد (این کلاس توسط سیستم‌عامل ساخته می‌شود)، نمی‌تواند mNetwork و mPhotoManager را مقداردهی کند. تزریق یا مقداردهی وابستگی‌ها وظیفه یک کامپوننت است.

مرحله ۳: ایجاد یک Component

بدون توضیح اضافه، به سراغ نوشتن کامپوننت می‌رویم.

@Singleton
@Component(modules={AppModule.class})
public interface AppComponent {  
   void inject(MainActivity activity);
}

همان‌طور که در ابتدای نوشته توضیح دادم، کامپوننت‌ها پل ارتباطی ماژول‌ها و وابستگی‌ها هستند. با استفاده از عبارت {modules={AppModule.class، یک طرفِ این پل به ماژول موردنظر ما وصل می‌شود. بدین ترتیب مشخص می‌شود که قرار است اشیاء ساخته شده توسط کدام ماژول ایجاد شوند. طرف دیگر این پل، کلاس MainActivity است که قصد داشتیم وابستگی‌ها را درون آن تزریق کنیم. به همین دلیل متودی به نام inject را تعریف کرده‌ایم که یک ورودی از نوع MainActivity‍ می‌پذیرد.

کنار هم چیدن تکه‌های پازل

ما همه اجزاء موردنیاز Dagger را تعریف کرده‌ایم. حالا پروژه خود را Rebuild کنید تا Dagger کلاس‌های موردنیاز ما را تولید (generate) کند. ما AppComponent‌ را به شکل یک interface تعریف کرده بودیم. بعد از build شدن پروژه، کلاسی با نام DaggerAppComponent در اختیار ما قرار خواهد گرفت (این کلاس به طور خودکار توسط Dagger‌ تولید می‌شود). Dagger در واقع اینترفیسِ AppComponent را پیاده‌سازی (implement) کرده و برای نام‌گذاری، واژهٔ Dagger را به ابتدای نام کلاس می‌چسباند.

استفاده از کلاس (کامپوننت) تولید شده

هدف ما از ساختن AppModule، ایجاد کردن singletonهای واقعی بود. به همین خاطر باید در جایی از DaggerAppComponet استفاده کنیم که در تمام چرخهٔ عمر برنامه، در دسترس باشد.

بهترین راه این است که کلاسی را جایگزین کلاسِ پیش‌فرض Application کنیم و داخل آن کلاس، یک نمونه از کامپوننت تولید شده ایجاد نماییم. بنابراین AndroidManifest.xml را بدین شکل تغییر دهید:

⋮
<application  
      android:name=".MyApplication"
⋮

حالا کلاس MyApplication را پیاده‌سازی کرده و کامپوننت تولید شده را در درون آن، راه‌اندازی می‌کنیم.

public class MyApplication extends Application {

  private AppComponent mAppComponent;

  @Override
  public void onCreate() {
    super.onCreate();

    mAppComponent = DaggerAppComponent.builder()
      .appModule(new AppModule(this)) 
      .build();
    }

    public AppComponent getComponent() {
       return mAppComponent;
    }
}

همان‌طور که می‌بینید برای راه‌اندازی کامپوننت، لازم است ماژول‌هایی که بخشی از آن هستند (در این‌جا AppModule) نیز راه‌اندازی شوند. در نهایت متود build، یک نمونه از کامپوننت موردنظر ما را می‌سازد.

همچنین ما در این‌جا متود دیگری را به نام ()getComponent تعریف کرده‌ایم. در بخش بعد خواهید دید که چگونه از این متود استفاده می‌کنیم.

تزریق وابستگی

همه‌چیز آماده است؛ جز این‌که باید به DaggerAppComponent این فرصت را بدهیم تا تزریق را انجام دهد (Dagger هنوز نمی‌تواند وابستگی‌های تعریف شده در MainActivity را مقداردهی کند). اضافه کردن یک خط به MainActivity کار را تمام می‌کند:

public void onCreate(){  
  ((MyApplication) getApplication()).getComponent().inject(this);
}

singletonهای نه‌چندان واقعی (singletonهای محلی)

اگر بخواهیم یک شیء در چرخهٔ حیات یک Fragment یکتا یا singleton باشد، می‌توانیم کامپوننت دیگری بنویسیم و به جای MyApplication، آن را در هر Fragmentی راه‌اندازی کنیم. بدین ترتیب چند singleton نه‌چندان واقعی ایجاد می‌شود (اگر چند Fragment‌ داشته باشیم، در هر Fragment یک نمونه متمایز از آن شیء ساخته می‌شود). این singletonها در یک محدودهٔ مشخص یکتا هستند.

چطور singletonهای محلی را شناسایی کنیم؟

انوتیشین‌های Singleton@ گمراه‌کننده هستند. اگر قرار باشد با همین رویه کار را ادامه بدهیم، در کل برنامه با تعداد زیادی Singleton@ سروکار داریم. با این‌حال فقط تعداد محدودی از آن‌ها در چرخهٔ حیات برنامه singleton هستند. محدوده به کارگیری هر کامپوننت، می‌تواند ما را حسابی گمراه کند.

خبر خوب این است که ما می‌توانیم انوتیشن‌های دلخواه خودمان را بسازیم تا برنامه خواناتر شود. Singleton@ تنها انوتیشنی است که به طور پیش‌فرض در زبان جاوا تعریف شده. من در این‌جا برای علامت زدن اشیائی که در محدوده Fragmentها ایجاد می‌شوند، یک انوتیشن جدید تعریف می‌کنم (FragmentScope.java).

@Scope
@Retention(RetentionPolicy.RUNTIME)
public @interface FragmentScope {  
}

محدوده‌های دلخواه، با استفاده از انوتیشین Scope@ تعریف می‌شوند. حالا می‌توانم برای علامت زدن اشیائی که در محدوده یک فرگمنت singleton هستند، از FragmentScope@ استفاده کنم.

@Module
public class FragmentModule{  
  @Provides @FragmentScope
  Object provideSomething(){
    return new Object();
  }
}

جمع‌بندی

به خودتان فرصت بدهید تا مفاهیم بالا در ذهنتان ته‌نشین شود. توصیه می‌کنم به چند پروژهٔ واقعی که از Dagger 2 استفاده می‌کنند، نگاهی بیندازید (مثل این پروژه). بعد خودتان یک پروژه واقعی بنویسید و کد مربوط به کلاس‌هایی را که Dagger تولید می‌کند، بخوانید تا متوجه شوید که چه اتفاقاتی در پشت پرده رخ می‌دهد.

سایر قابلیت‌های Dagger

Dagger قابلیت‌های دیگری هم دارد که من در این نوشته به آن نپرداخته‌ام. مثلاً این‌که ما می‌توانیم یک کامپوننت (فرزند) را به یک کامپوننت دیگر (پدر) وابسته کنیم. در اغلب پروژه‌های واقعی، ما به بسیاری از این قابلیت‌ها احتیاج پیدا می‌کنیم. اما برای ساده‌تر شدن روند یادگیری، توصیه می‌کنم در ابتدای کار با مباحث پایه‌ای دست‌وپنجه نرم کنید.