آموزش HILT در اندروید - قسمت 1

چند وقتی میشه که Google برای DI ابزار جدیدی رو به نام HILT معرفی کرده (اگه نمی‌دونید DI چیه نگران نباشید ، جلوتر میگم) ، در سری مقالات قبلی به معرفی Dagger Android پرداختیم که اصلی ترین ابزار برای DI در اندروید هست (ابزارهای دیگه مثل Koin و ... هم هستند) ، HILT هم در واقع ورژن پیشرفته و ساده ترِ Dagger هست (یک نوع ورژن از Dagger در گذشته وجود داشته ، گوگل از روی اون ورژن Dagger2 رو درست می‌کنه و بعد برای اینکه توسعه دهنده های اندروید راحت تر باشن یک ورژن مخصوص اندروید هم توسعه میده و الان هم رفته سراغ HILT) .

خب سریع یک مروری بکنیم که DI و اینها چی بودند اصلا !

تزریق وابستگی ها - Dependency Injection

وابستگی چیه ؟ اولا که وقتی میگیم Dependency منظورمون اون چیزایی که شما تو فایل gradle می‌نویسید نیست ! هر کلاس می‌تواند به کلاس های دیگر وابسته باشد ، مثلا کلاس "ماشین" وابستگی به کلاس "چرخ" داره و این وابستگی باید به اون تزریق بشه ، یکی از راه ها اینه که ما تو "سازنده" یا "constructor" کلاسِ "ماشین" ، اِلمانِ کلاس "چرخ" رو بیاریم و ما با این کار به نوعی عملِ "تزریق وابستگی رو انجام دادیم" ، ولی اگه بعدا سازنده رو عوض کنیم چی ؟ هزار جای کد باید این تغییر رو اعمال کنیم ! پس بهترین کار اینه که به نحوی بیاییم این عملیات رو در لایه های بالاتری از انتزاع (Abstraction) انجام بدیم .

ابزارِ HILT

در لغت Hilt به معنای دسته‌ی خنجره (و Dagger به معنای خنجر) ، حالا اینکه چرا این اسم رو این سری انتخاب کردند نمی‌دونم ?? ، هدف ایجادِ HILT راحت تر کردنِ استفاده از Dagger2 برای توسعه دهنده هاست ، اگه قبلا با Dagger Android یا حتی ورژن قبل اون که برای جاوا بوده کار کرده باشید می‌دونید که مصائب زیادی داره :/ ، در عوض با HILT ما بیشترِ کارامون رو با انوتیشن ها انجام می‌دیم .

شروعِ کار

ابتدایِ امر باید کتاب‌خونه HILT رو به پروژه اضافه کنیم ، اول به gradle اصلی برید :

classpath 'com.google.dagger:hilt-android-gradle-plugin:2.28-alpha'

و بعد به gradle app برید ، اول دو پلاگین مورد نیاز رو در بالای پروژه اضافه کنید :

apply plugin: 'kotlin-kapt'
apply plugin: 'dagger.hilt.android.plugin'

و بعد dependency های مربوط رو اضافه می‌کنیم :

implementation "com.google.dagger:hilt-android:2.28-alpha"
kapt "com.google.dagger:hilt-android-compiler:2.28-alpha"

ابزارِ HILT از جاوا 8 استفاده می‌کنه پس این مورد رو هم باید اضافه کنیم :

android {
// other things . . .
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

اگه سری مقالاتِ Dagger2 که در ویرگول نوشتم رو خونده باشید با Component آشنایید ، در Dagger2 ما یک Component اصلی به نام AppComponent داشتیم و یک سری SubComponent (که زیرمجموعه Component دیگری حساب می‌شدند) ، قضیه به این صورت بود که اگه یک Dependency توسط AppComponent تهیه (Provide) میشد تمامی SubComponent ها به اون دسترسی داشتند ولی برعکس این موضوع صادق نبود ، مشکلی که در Dagger2 وجود داشت مکافاتی بود که شما برای درست کردن این Component ها باید تحمل می‌کردید ، چیز دیگه ای هم به نام Module داشتیم ، ماژول در Dagger کلاسی بود که برای ما Dependency ها رو Provide می‌کرد ، یعنی چی ؟ فرض کنید یه Dependency پیچیده مثل Retrofit یا ... رو بخواید تزریق کنید ، شما از تریق Module این Dependency رو می‌سازید و بعد به کلاس مورد نظر Inject می‌کنید .

معماری کلی Dagger2
معماری کلی Dagger2

ما در Class ها به Dependency ها نیاز داریم ، Component برای ما این Dependency ها رو تزریق می‌کنه و خودِ Component به واسطه Module اونا رو Provide می‌کنه ، Scope چی بود ؟ معنی لغوی Scope یعنی محدوده ، پس کارش اینه که بیاد یک چیزی رو برای ما محدود کنه ، ما با Scope کردن میاییم مشخص می‌کنیم فلان Dependency فقط برای فلان Component هست و بهمان Dependency فقط برای بهمان Component ، حالا در Hilt چطوری باید Module و Component و Scope رو ایجاد کنیم ؟ حالا این Inject یا تزریق در Hilt چطوریه ، مثلا فرض کنید می‌خوایم به کلاسِ Application قابلیت Inject کردن رو بدیم (یعنی توش وابستگی تزریق کنیم) ؟

https://gist.github.com/sasssass/6cbe440658d64c866fcc963484179487

و تمام :) باقی کارها توسط خودِ HILT هندل میشه و دیگه لازم نیست مثل Dagger کلی کد بنویسید و بعد یه بارم Build بگیرید که کلاس ها ساخته بشن و ... ، با همین یه دونه انوتیشن ما کلاسِ App رو وارد HILT کردیم و حالا می‌تونیم توش Dependency ها رو تزریق کنیم !

ما دو نوع تزریق (Inject) وابستگی داریم ، یکی به صورت Field و یکی به صورت Constructor ، در واقع Field Injection حالت عادی قضیه حساب میشه ، مثلا شما در کدهاتون با انوتیشن Inject مشخص می‌کنید که برای این شئ باید Inject صورت بگیره اما در Constructor Injection شما Constructor اون شئ رو با کلمه Inject مشخص می‌کنید و بعد از اون هر زمانی که اون شئ Inject بشه ورودی هاش هم Inject میشند ، اگر درست متوجه نشدید مثال پایین کمک می‌کنه :

https://gist.github.com/sasssass/2c3ace7faa55f4c997c5721af9519617

در این کد SomeClass به صورتِ Field و SomeOtherClass به صورت Constructor به درون کلاس ها Inject شدند ، حالا فرض کنید بخوایم به درون یک Activity یا ... یک شئ رو Inject کنیم ، چه کار باید بکنیم ؟

ما با مشخص کردن HiltAndroidApp برای کلاسِ App این کلاس رو به داخل گرافِ HILT اضافه کردیم ، حالا همین کار رو با انوتیشن دیگه ای باید انجام بدیم و این انوتیشن ، AndroidEntryPoint هست ، این انوتیشن می‌تونه برای Activity ، Fragment ، Service ، BroadcastReceiver و View استفاده بشه ، به کدِ زیر دقت کنید :

https://gist.github.com/sasssass/04b3818a384798d958aa445d8a09b915

در این کد ما MainActivity رو به گرافِ HILT اضافه کردیم (منظورم اینه که الان MainActivity زیر مجموعه App حساب میشه از نظر HILT) و با اضافه کردنش به گراف ، توانایی Inject کردن رو بهش دادیم ، شئِ BlawBlaw به صورت Field به درون MainActivity تزریق شده ، از طرفی در شیء BlawBlaw ما یک متغیر به نام SomeClass داریم که اون هم باز به صورت Field تزریق شده ، در کلاسِ SomeClass اما دیگه تزریق رو به صورت Field انجام ندادیم ، در SomeClass کلاسِ دیگه ای به اسم SomeOtherClass به صورت Constructor به درون SomeClass تزریق شده ! امیدوارم با این مثال قشنگ تفاوت این دو براتون جا افتاده باشه !

نکته : این مثال ها صرفا آموزشی هستند ، در سری مقالات بعدی مثال های کاربردی و ایجاد یک پروژه با HILT رو مورد بررسی قرار می‌دیم

ایجاد Component

در Dagger ما مستقیم خودِ Component رو ایجاد می‌کردیم ولی در HILT این‌کار رو نمی‌کنیم ، در HILT ما از یک سری Component از پیش ساخته شده استفاده می‌کنیم ، یعنی به جای اینکه یک کلاس FolanComponent بسازیم فقط میاییم اون Dependency مورد نظر رو با یک انوتیشن مشخص می‌کنیم تا خودش عملیات ساخت Component مورد نظر و وصل کردنش به اون کلاس رو انجم بده ، این انوتیشن ها مشخص کننده Scope و محدوده مورد نظر ما هستند :

ارث بری Componentها
ارث بری Componentها

اگر به شکل دقت کنید می‌بینید یک حالت درخت/ارث بری برای ما کشیده شده ، یعنی چی ؟ فرض کنید ما یک Dependency داریم ، اون رو باید با Scopeای با نام ActivityScope مشخص کنیم ، حالا وقتی این کار رو کردیم اون Dependency فقط می‌تونه از طریق ActivityComponent به پایین (FragmentComponent و ViewWithFragmentComponent) تزریق بشه ، یعنی Dependency ای که متخص Activity هست رو در کلاس Application نمی‌تونید استفاده کنید ولی برعکسش مشکلی نداره . در مورد قسمت Legend هم برای زمانیه که بخواید یک Component دلخواه بسازید که کاری باهاش نداریم . خب پس تا اینجای کار فهمیدیم که لزومی به ساخت Component نیست و از Componentهای از قبل ساخته شده استفاده می‌کنیم ، حالا بریم سراغ ماژول .

ایجاد Module

فرض کنید یک کلاسِ پیچیده داریم (ComplexModel) که دیگه نمی‌تونیم مثل روش های مثال های قبلی خیلی راحت اونو Inject کنیم بلکه نیاز به تابعی داریم که این کلاس رو برامون تهیه یا Provide کنه ، صورت مساله به این شکله که قصد داریم یک Module به نام ActivityModule بسازیم که وظیفه اش تزریق وابستگی‌ها به کلاس MainActivity باشه ، به کد زیر دقت کنید :

https://gist.github.com/sasssass/c7e5ac37decc30f0530f45b461310fd9

در این کد اول از همه MainActivity رو داخل گرافِ HILT کردیم (با AndroidEntryPoint) ، بعد از اون ActivityModule رو ساختیم ، وقتی شما می‌خواید مشخص کنید Module مورد نظر مربوط به کدوم Component باشه باید از InstallIn استفاده کنید و توش جنسِ کلاسِ اون Component رو بنویسید (برای اسم های Componentها به عکس های بالاتر نگاه کنید) ، خودِ Module رو هم با انوتیشن Module مشخص می‌کنیم که HILT بفهمه این کلاس یک Module حساب میشه ، حالا برای تهیه Dependency دو راه داریم ، یکی استفاده از Bind یکی استفاده از Provide (که فعلا ما با همین Provide کار داریم) ، تابعی که داره برای ما Provide رو انجام میده provideComplexModel هست که میاد یک ComplexModel رو برای ما برمی‌گردونه ، خودِ این تابع رو هم می‌تونیم Scopeگذاری کنیم (می‌تونیم هم نکنیم چون به صورت پیش فرض این خودش می‌فهمه) و بعد در کلاسِ MainActivity عملیات Inject رو انجام می‌دیم .

نکته : HILT به صورت Compile-Time کدها رو بررسی می‌کنند پس اگر خطایی وجود داشته باشه اجازه اجرای برنامه داده نمیشه و برناه Build نمیشه پس نگران جا به جا نوشتن Scopeها و ... نباشید
نکته : در موردِ Bind نمی‌خوام توضیحی بدم ، در همین حد بدونید که اگه از Bind استفاده کنید نیازی نیست بدنه تابع رو بنویسید و به همین خاطر نمیشه همیشه ازش استفاده کرد (مثلا اگه بخواید Retrofit رو Inject کنید) پس بهتره از همون Provide استفاده کنید

در قسمت های بعدی از سری مقالات HILT ، بیشتر این مبحث رو باز می‌کنیم . . .

من رو در لینکدین و اینستاگرام دنبال کنید ???

اگه دوست داشتید می‌تونید به صفحه Spotify بنده هم برید و موسیقی های منو گوش بدید ???