Dagger 2 در اندروید (بخش اول) — راهنمای پیشرفته

۲۰۷ بازدید
آخرین به‌روزرسانی: ۲۷ شهریور ۱۴۰۲
زمان مطالعه: ۶ دقیقه
Dagger 2 در اندروید (بخش اول) — راهنمای پیشرفته

Dagger یک موضوع متناقض در جامعه اندروید محسوب می‌شود. بحث‌های و نزاع‌های بی‌شماری پیرامون آن شکل گرفته است و بسیاری از افراد بارها نظرخود را در مورد آن تغییر داده‌اند. اغلب برنامه‌نویس‌ها سابقه‌ای از توسعه دهندگی دارند که در آن تزریق وابستگی کاری آسان محسوب می‌شود و از این رو پذیرش پیچیدگی Dagger به زمان بیشتری نیاز دارد که در اغلب موارد بسیار بیشتر از مقدار مورد انتظار است.

امروزه ما در کاتلین با جایگزین‌های جذابی مانند Koin یا Kodein مواجه هستیم و برخی افراد در این مسیر حرکت می‌کنند. با این حال شاید اطلاق واژه جایگزین برای این موارد درست نباشد، زیرا آن‌ها بیشتر سرویس یاب (service locator) هستند تا یک تزریق کننده واقعی وابستگی.

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

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

چند توصیه کلی

  • در مورد همه کلاس‌هایی که می‌نویسید از «تزریق ساختار» (Construction Injection) استفاده کنید.
  • به جای کامپوننت‌های فرعی، حوزه تعریف (Scope) را با استفاده از ViewModel کنترل کنید.
  • وجود dagger-android را فراموش کنید.

سرآغاز

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

تزریق ساختار

یکی از مواردی که در Dagger کاملاً نادیده گرفته شده، بحث «تزریق ساختار» (Construction Injection) است. برخی اوقات ما چنان به استفاده از ماژول‌ها عادت می‌کنیم که فراموش می‌کنیم گزینه اجتناب کامل از نوشتن «متدهای سازنده» (Constructor) نیز وجود دارد. به نظر می‌رسد این دقیقاً مزیت کلیدی Dagger نسبت به Koin و Kodein است.

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

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

@Reusable
class BestPostFinder @Inject constructor() {
    ...
}

تا این که کدهایی به صورت زیر بنویسیم:

@Module
object PostModule {

    @JvmStatic @Provides @Reusable
    fun provideBestPostFinder() = BestPostFinder()
}

هنگامی که شروع به افزودن وابستگی‌هایی بکنید که خودشان فهرست بزرگی از وابستگی‌ها دارند، ماژول‌ها به طور کنترل نشده‌ای شروع به بزرگ شدن می‌کنند و خیلی زود به وضعیت نامناسبی می‌رسید.

کامپوننت‌های فرعی و حوزه‌های تعریف

اگر «کامپوننت‌های فرعی» (Subcomponents) حائز اهمیت بالایی برای شما هستند، راهبرد فوق ممکن است نیازهای شما را رفع نکند، اما در اغلب پروژه‌ها می‌تواند راهگشا باشد. در واقع اغلب موارد کاربرد کامپوننت‌های فرعی ضرورتی ندارد؛ اما به دلیل بهبود ساده در سازماندهی وابستگی‌ها استفاده می‌شوند. در مستندات (+) ایده استفاده از «کامپوننت‌های فرعی به جای کپسوله‌سازی» مطرح شده است؛ اما بسیاری از افراد با این ایده مخالف هستند. افزودن کدهای تکراری سازنده که در صورت استفاده از تزریق ساختار قابل اجتناب است به سازمان‌یافتگی بیشتر کد به خصوص در بلند مدت کمک می‌کند.

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

با این وجود برخی کاربردهای دیگر مانند مواردی که می‌خواهیم یک Activity در گراف داشته باشیم، نیز هستند که ممکن است کامپوننت‌های فرعی در آن‌ها مفید باشند. توجه کنید که ما استفاده از کامپوننت‌های فرعی در همه جا را به طور کامل رد نمی‌کنیم؛ اما اشاره می‌کنیم که اگر از آن‌ها صرفاً برای سازماندهی وابستگی‌ها یا ایجاد حوزه‌های تعریف سفارشی Activity/Fragment استفاده می‌کنید، احتمالاً فرصت ساده‌تر ساختن تنظیمات Dagger را از دست می‌دهید.

تزریق وابستگی در ViewModel با استفاده از Dagger ممکن است در ابتدا پیچیده به نظر بیاید. اگر به کدهای نمونه معماری اندروید ارائه شده از سوی گوگل (+) نگاه کنید مثالی را می‌یابید که شامل نقش‌آفرینی هر دو مورد ViewModels و Dagger است؛ اما اگر به نمونه‌های کامپوننت معماری (+) نگاه کنید، GithubBrowserSample (+) را می‌بینید که آن‌ها را با هم استفاده کرده است. اگر به مثال GithubViewModelFactory (+) نگاه کنید ممکن است به وحشت بیفتید.

در نهایت باید اشاره کنیم که روش مورد تأکید ما، می‌تواند در عمل بسیار ساده‌تر از آن چیزی باشد که تصور می‌کنید. همه آن چه نیاز دارید این است که یک annotation به صورت Inject@ در سازنده ViewModel خود اضافه کنید و ViewModelFactory زیبای زیر این کار را برای شما انجام می‌دهد:

class ViewModelFactory<VM : ViewModel> @Inject constructor(
    private val viewModel: Lazy<VM>
) : ViewModelProvider.Factory {

    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>) = viewModel.get() as T
}

اینک می‌توانید به سادگی آن ViewModelFactory را تزریق کنید و از آن برای ساخت هر ViewModel که می‌خواهید به صورت زیر بهره بگیرید:

class BestPostActivity : AppCompatActivity() {

    private val viewModel by lazy {
        ViewModelProviders
            .of(this, injector.bestPostViewModelFactory())
            .get(BestPostViewModel::class.java)
    }

    ...
}

با توجه به این که ما عملاً نیازی به نگهداری ارجاعی برای ViewModel برای دسترسی‌های بعدی نداریم؛ بهتر است به جای پیچیدن ViewModel در یک Lazy در ViewModelFactory، آن را در یک Provider بپیچید. دلیل این که آن را در وهله نخست به آن صورت می‌پیچیم آن است که برای مثال لازم نیست ViewModel و وابستگی‌هایش را در طی تغییر جهت‌گیری، مجدداً ایجاد کنیم. اگر فکر می‌کنید این injector عجیب است، در ادامه در مورد آن بیشتر توضیح داده‌ایم.

ما می‌توانیم این وضعیت را با اپلیکیشن ارائه شده در کنفرانس I/O 2018 گوگل (Google I/O 2018 app) مقایسه کنیم. این تنظیمات اندکی متفاوت از GithubBrowserSample است. و ساده‌تر به حساب می‌آید؛ اما چند factory (به صورت یکی برای هر ViewModel) وجود دارد و همچنان پیچیده‌تر از چیزی است که ما اجرا کرده‌ایم.

اگر به تنظیمات ارائه شده در این نوشته علاقه‌مند هستید، پیشنهاد می‌کنیم مورد ارائه شده از سوی Gabor Varadi (+) را نیز بررسی کنید. ما برای این تنظیمات نیز در اپلیکیشن خود یک شاخه (+) ایجاد کرده‌ایم. اپلیکیشن I/O گوگل و هم چنین GithubBrowserSample از dagger-android استفاده کرده‌اند، بنابراین در ادامه در مورد آن صحبت می‌کنیم.

dagger-android

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

((SomeApplicationBaseType) getContext().getApplicationContext())
        .getApplicationComponent()
        .newActivityComponentBuilder()
        .activity(this)
        .build()
.inject(this);

در واقع کد فوق نشان دهنده سناریوی بدترین حالت است و در اغلب موارد وضعیت به این بدی نیست. اما در هر حال این یک واقعیت معتبر است. به طور معمول Activity (یا Fragment) نباید در مورد تزریق کننده خودشان اطلاعاتی داشته باشند. با این وجود، ما برای غلبه بر این وضعیت به همه مواردی که dagger-android ارائه می‌کند به خصوص در مورد استفاده از کامپوننت‌های فرعی، نیاز نداریم. به جای آن می‌توانیم از کد زیر استفاده کنیم:

interface DaggerComponentProvider {

    val component: ApplicationComponent
}

val Activity.injector get() = (application as DaggerComponentProvider).component

در این صورت کلاس اپلیکیشن، DaggerComponentProvider را پیاده‌سازی می‌کند و component را از طریق آن ارائه می‌کند و به لطف سادگی بیش از حد این اکستنشن، می‌توانیم چیزهایی که در یک Activity قرار دارند را با یک  ()injector.inject ساده تزریق کنیم. در این حالت Activity چیزی در مورد تزریق کننده خود نمی‌داند و پیکربندی Dagger ما نیز همچنان ساده و درک آن آسان است.

سخن پایانی

آنچه در مورد dagger-android بیشتر آزار می‌دهد، میزان دشواری راه‌اندازی آن است و دلیل شهرت منفی Dagger در مورد پیچیدگی نیز همین مورد است. اگر فکر می‌کنید در مورد کنار گذاشتن dagger-android از پروژه‌تان به دلایل بیشتری نیاز دارید می‌توانید به موارد مطرح شده در این مقاله (+) رجوع کنید.

بهتر بود در مستندات dagger-android در مورد این موارد بیشتر توضیح داده می‌شد؛ اما متأسفانه آن‌ها بدون در نظر گرفتن این موارد صرفاً تلاش کرده‌اند این کتابخانه را به کاربر بقبولانند. با این حال اگر شما در هر مرحله از توسعه اپلیکیشن خود، به یک سناریوی با قابلیت ماژوله سازی بالا نیاز خواهید داشت، در این صورت می‌توانید به استفاده از dagger-android فکر کنید.

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

مستندات Dagger چندان عالی نیست. البته در گذشته بسیار بدتر بود و به مرور برخی بهبودها در آن صورت گرفته است و امیدواریم در آینده فردی پیدا شود که بتواند همه مباحث مرتبط با Dagger را در آن وارد کند. در هر صورت تا آن زمان می‌توانید از راهنمایی‌های مطرح شده در این نوشته استفاده کنید.

اگر این مطلب برایتان مفید بوده است، آموزش‌های زیر نیز به شما پیشنهاد می‌شوند:

==

بر اساس رای ۲ نفر
آیا این مطلب برای شما مفید بود؟
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
proandroiddev
نظر شما چیست؟

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