اگر پروژه ای دارید که توش از دگر استفاده میکنید و نمیدونید مهاجرت به هیلت رو از کجا شروع کنید جای درستی اومدید. مخصوصا اگه پروژتون بزرگه و وقت مایگریت(مهاجرت) کردن همه قسمت ها به هیلت رو ندارید یا میخواید فقط قسمت های جدید رو با هیلت انجام بدید.
ما توی این مقاله میخوایم راهی برای مهاجرت به هیلت به صورت قسمت قسمت برای پروژه هایی که با Component Dependencies کار میکنن معرفی کنیم.
این مقاله برای کسایی که با دگر و هیلت ی مقداری آشنایی دارن مناسب هست. اگر شما با این کتابخونهها آشنایی کافی ندارید میتونید قبلش سری به اینجا بزنید تا آشنایی بیشتری پیدا کنید.
کمی پیش Google کتابخونه ی جدیدش به اسم hilt رو برای DI معرفی کرد. از ویژگی های این کتابخونه میشه به سادگیش و learning curve پایینش نسبت به دگر اشاره کرد.
هیلت بر پایه ی دگر ساخته شده و در درونش از دگر استفاده میکنه. بخاطر همین این دوتا کتابخونه میتونن در کنار هم در ی پروژه استفاده بشن.
این مقاله کمکتون میکنه پروژه ای که بر پایه ی دگر هست رو به صورت دوره ای به هیلت مایگریت کنید. یعنی مجبور نیستیم همه ی کدهای DI رو به هیلت مایگریت کنید و تیکه تیکه این کارو انجام میدیم.
پیش فرض این مقاله اینه که پروژمون بر پایه ی Dagger Componentها ست و نه SubComponentها و ممکنه همش یا قسمت هاییش برای SubComponentها جواب نده.
ما میتونیم یک یا چند component رو به هیلت مایگریت کنیم ولی بقیه componentها دگری باشند و کدهای قسمت هیلت و دگر با هم دیگه کار میکنن ولی این نکته رو باید بدونیم که کدهای قسمت دگر میتونن از کدهای هیلت استفاده کنن ولی برعکسش صادق نیست. کدهای هیلت نمیتونن به componentهای دگری وابستگی داشته باشن. بر همین اساس مهاجرت به هیلت رو باید از قسمت های پایه ای کدمون شروع کنیم. در درجه اول باید از کلاس Application شروع کنیم، چون این کلاس پایه ی ساخت گراف دگر هست.
قبل شروع قسمت اصلی میخوام چندتا مفهوم رو که تو مقاله استفاده شدن با هم مرور کنیم:
یک Binding: به طور کلی به هر شی که به گراف دگر اضافه میشه Binding میگیم. این مفهوم رو با انوتیشن Binds@
قاطی نکنید که یکسان نیستن.
دوم EntryPoint: عنصری در هیلت وجود داره به اسم Entrypoint و این عنصر باعث میشه بتونیم Bindingها رو به خارج گراف هیلت ارائه بدیم (مثلا برای اینکه در دگر ازشون استفاده کنیم). از این لحاظ عملکرد Entrypoint شبیه Componentها ست. EntryPointها با انوتیشن EntryPoint@
مشخص میشن.
سوم Componentهای پیش فرض هیلت: در هیلت ی سری Component به صورت پیش فرض وجود داره که با کلاس های اندرویدی متناظر هستن و LifeCycle یکسانی دارن(با هم ساخته و پاک میشن).
مثلا FragmentComponent با Fragment متناظر هست. این Componentها که در زیر لیست شون اومده برای بسیاری از کاربردها کافیه و دیگه معمولا ما نیاز به نوشتن Custom Component نداریم. همچنین این Componentها Scopeهای متناظرشون رو هم دارن که تو جدول زیر لیست شدن:
در نتیجه ما دیگه در هیلت (معمولا) Component نمینویسیم و فقط Module و EntryPoint مینویسیم و مشخص میکنیم که این Module یا EntryPoint مربوط به کدوم یکی از Componentهای جدول بالا هست. برای مثال برای اینکه ماژول زیر در SingletonComponent
اضافه بشه به صورت زیر عمل میکنیم:
خوب تعاریف تموم شد. بریم که پروژه رو هیلتی کنیم :)
با فرض داشتن ی پروژه ی دگری که شامل ی سری Component که به هم Depend هستن شروع میکنیم و مراحل رو پیش میبریم.
در مرحله اول دیپندنسی های هیلت رو اضافه میکنیم:
اگر از kapt استفاده نمیکنید kapt رو با annotationProcessor جایگزین کنید.
اگر در گریدل-ماژولی که دارید روش کار میکنید کل کدهای اون ماژول رو به هیلت مایگریت خواهید کرد میتونید دپندنسی های دگر رو هم پاک کنید:
اگر در ماژول تون از Workerها استفاده میکنید این دپندنسی ها رو هم اضافه کنید:
اگر کامپوننت اندرویدی از جمله Activity, Service, Fragment یا BroadcastReceiver دارید plugin dagger.hilt.android.plugin
رو هم به گریدل ماژولتون تو قسمت pluginها اضافه کنید:
حالا میریم سراغ کلاس Application و انوتیشن HiltAndroidApp
رو بالاش اضافه میکنیم:
اگر کلاس Applicationتون اینترفیس Configuration.Provider
رو پیاده سازی میکنه و میخواید Workerی رو به Hilt مایگریت کنید به نکته آخر مقاله مراجعه کنید.
ما اینجا یک کامپوننت رو به هیلت مایگریت میکنیم و بعد میبینیم که چطور یک کامپوننت دگر ازش میتونه استفاده کنه.
نکته: فقط میتونید کامپوننتهایی رو به هیلت مایگریت کنید که dependency به ی کامپوننت دگری نداشته باشن(با استفاده از انوتیشین Component@ قسمت dependencies کامپوننتها رو به هم depend میکنیم). اگر دارن اول باید اون dependency رو مایگریت کنید.
برای مثال کامپوننت زیر رو در نظر بگیرید:
در مرحلهی اول سراغ قسمت modules میریم. اگر در بین ماژول ها ماژول AndroidInjectionModule وجود داره پاکش کنید(در dagger-android ازش استفاده میشه).
بعد میریم سراغ تک تک moduleها و اونها رو هم به هیلت مایگریت میکنیم. مایگریت ماژول ها رو پایینتر در مرحله ۳ گفتم و اگر ماژولی رو میخواید مایگریت بدید همین حالا سراغ مرحله ی ۳ برید. ?
بعد مایگریت کردن همه ی ماژولها میتونیم کل قسمت modules رو پاک کنیم:
در مرحلهی بعد میریم سراغ bindingهایی(متدهایی) که کامپوننت تعریف کرده، که اینجا دوتا هستند.
اگر کامپوننتی به این کامپوننت وابستگی داره، این متدها باید بمونن، اگر نه میتونیم حذفشون کنیم. تو این مثال فرض میکنیم که همچین کامپوننتی وجود داره.
در مرحله بعد اگر کامپوننت اینترفیس AndroidInjector
رو پیاده سازی میکنه اون رو هم حذف میکنیم:
در مرحلهی بعد انوتیشن Component@
و هر چیزی که داخلش هست رو حذف میکنیم.
حتی اگه Component@ قسمت dependencies داره اون رو هم حذف میکنیم. چون فرضمون این هست که کامپوننتهای داخل قسمت dependencies قبلا به هیلت مایگریت شدن.
اگر کامپوننت انوتیشن Scope داره اون رو هم حذف میکنیم و بجاش دوتا انوتیشن جدید میذاریم:
انوتیشن EntryPoint@
و انوتیشن InstallIn@
. در این انوتیشن قراره مشخص کنیم که این کامپوننت برای چه کلاس اندرویدیی کار میکنه. مثلا اگه این کامپوننت قرار فیلدهای ی Activity رو Inject کنه از ActivityComponent استفاده میکنیم. به همین منوال برای همه کلاس های اندرویدی دیگه هم component از پیش تعریف شدهای وجود داره که همه شون رو توی جدول پایین آووردم:
حالت قبل و بعد از تغییر انوتیشن ها رو زیر میبینیم:
حالا دیگه DiscountCodeComponent ی کامپوننت نیست و در واقع EntryPoint هست ولی همونطور که بالاتر گفتیم کاربردی مشابه کامپوننت داره. در نتیجه اگر در ی کامپوننت دگری ازش به عنوان dependency استفاده شده لازم نیست روی قسمت دگر کاری بکنید. دگر میتونه از Entrypoint جدیدمون مثل component قدیمی استفاده کنه. مثل بنز کار میکنه. حالا میتونید کامپوننت رو Renameش کنید و بجای Component، بنویسید EntryPoint.
خود حالا میخوایم ی ماژول رو مایگریت کنیم. برای مثال ماژول زیر رو در نظر بگیرید:
۱. اگر ماژول دارای متدهایی هست که ViewModelی رو محیا میکنن اون متد رو حذف میکنیم و میریم سر کلاس ViewModel انوتیشن HiltViewModel@
رو اضافه میکنیم:
۲. اگر ماژول دارای متدی هست که ViewModelFactoryی رو محیا میکنه اون رو هم پاک میکنیم:
۳. اگر ماژول دارای متدهایی هست که انوتیشن ContributesAndroidInjector@
دارند میریم سراغ کلاسی که داره return میکنه(Activity, Service, Fragment یا BroadCastReceiver) و کارهای زیر رو روش انجام میدیم:
۱. بالای کلاس انوتیشن AndroidEntryPoint@
میزاریم.
۲. اگر از ViewModelی در این کلاس استفاده شده که به هیلت مایگریت شده یا میخواد مایگریت بشه و برای ساختنش از ViewModelFactoryیی که Inject شده استفاده کردیم دیگه لازم نیست از ViewModelFactory استفاده کنیم. پس مثلا این:
میشه این:
به صورت پیش فرض برای ساختن ViewModelها از defaultViewModelProviderFactory استفاده میشه و خود هیلت defaultViewModelProviderFactory رو تغییر میده تا ViewModelها رو بتونه Inject کنه. اگر جایی به ViewModelFactory نیاز داشتیم(مثلا وقتی از StoreOwner غیر پیش فرض استفاده میکنیم)، میتونیم از defaultViewModelProviderFactory استفاده کنیم.
نهایتا متد دارای ContributesAndroidInjector@
رو هم از ماژولی که روش کار میکردیم پاک میکنیم:
۳. اگر ماژول دارای متدی هست که Workerی رو میسازه مراحل زیر رو روش انجام میدیم:
۱. میریم سراغ Worker و بالاش انوتیشن HiltWorker@
میزاریم.
۲. اگر دارای اینترفیس با انوتیشن AssistedInject.Factory@
هست اینترفیس رو پاک میکنیم.
۳. متد مربوط به Worker در ماژول رو هم پاک میکنیم.
۴. انوتیشن AssistedModule@
رو از بالای ماژول پاک میکنیم.
برای ادامه ماژول زیر رو به عنوان مثال در نظر بگیرید:
۵. اگر بعد انجام مراحل قبل ماژول خالی از متد شد پاکش میکنیم. در غیر این صورت بالای ماژول بجز انوتیشن Module@
انوتیشن InstallIn@
رو با کامپوننت متناظرش اضافه میکنیم.
در ماژول بالا داریم ی سرویس Retrofit رو محیا میکنیم که نمیشه حذفش کرد. به این ماژول در Activity نیاز داریم، در نتیجه InstallIn@
رو با پارامتر ActivityComponent بهش اضافه میکنیم:
۶. اگر ماژول دارای متدهای هست که Custom Scope داره Scopeش رو به Scope متناسب با کامپوننت داخل InstallIn@
تغییر میدیم. Scope متناسب با ActivityComponent اسکوپ ActivityScoped@
هست(بالاتر تو جدول لیست componentها و scopeهای جفت باهاشون رو آووردیم) :
توجه داشتیم باشید که اگر از Scope اشتباهی استفاده کنید پروژه بیلد نمیشه. مثلا نباید وقتی ازInstallIn(ActivityComponent::class)@
استفاده کردید، از اسکوپFragmentScoped@
بالای متدی استفاده کنید.
همونطور که بالاتر در مثال کامپوننت دیدید ممکنه برای کامپوننت از Custom Scope استفاده کرده باشیم. مثلا اینجا از DiscountCodeScope استفاده شده:
گفتیم که در هیلت معمولا نیازی به Custom Scope نداریم. در نتیجه باید کارکردهای Custom Scope مورد نظر رو پیدا کرده و با Scope مناسب جابه جا کنیم. مثلا روی ی کلاس که با constructor Injection ساخته میشه هم از DiscountCodeScope استفاده کرده بودیم که به ActivityScoped تغییرش میدیم:
همه ی استفادههای Custom Scope مورد نظر رو پیدا میکنیم و با scope مناسب جابهجاشون میکنیم تا استفاده ای از اون نمونه. بعد میتونیم خود اون scope رو هم پاک کنیم.
دوتا نکته مونده که بهتون میگم:
۱- اگر در ی گریدل ماژول هم از دگر و هم از Hilt استفاده میکنید باید به Hilt بگید که با Moduleهای دگری کاری نداشته باشه و اونها رو پروسس نکنه. این کارو با اضافه کردن یک flag در فایل buildمون انجام میدیم:
۲- اگر کلاس Applicationتون interface Configuration.Provider
رو پیادهسازی میکنه و میخواید همه Workerها رو از دگر به هیلت مایگریت کنید باید کلاس HiltWorkerFactory رو inject کنید و در getWorkManagerConfiguration ازش استفاده کنید:
ولی اگر میخواید قسمتی از workerها رو به هیلت مایگریت کنید و از ی همچین کلاسی برای WorkerFactory در دگر استفاده میکنید، باید WorkerFactoryیه هیلت رو هم با دگر ترکیب کنید. این کلاس رو برای این منظور نوشتم:
حالا کلاس Application رو اینجوری تغییر بدید تا از AggregatorWorkerFactory استفاده کنه:
احتمال داره ی سری متد با انوتیشنBinds@
در ماژول هاتون باقی مونده باشه. من پیشنهادم اینه که از لایببری hilt-binder استفاده کنید که خودش براتون این متدهایBinds@
رو تولید میکنه. این لایببری ی Hilt-extention هست و با استفاده از انوتیشنی که شما سر کلاساتون میزنید متوجه میشه که باید برای اون کلاس ماژول و متدBinds@
تولید کنه تا کار شما راحتتر بشه.
و تمام :) تونستیم ی کامپوننت رو به هیلت مایگریت کنیم.
مطالعه بیشتر:
https://dagger.dev/hilt/migration-guide.html
https://developer.android.com/codelabs/android-dagger-to-hilt